├── .gitignore ├── content ├── epub │ └── blue.epub ├── screenreader-html-code │ ├── cover.png │ ├── radio-buttons.mp3 │ ├── radio-buttons.css │ ├── publication.json │ ├── radio-buttons.json │ └── prism.css ├── audiobook │ ├── css │ │ └── blue.css │ ├── toc.html │ └── blue.json ├── The-Blue-Fairy-Book-with-sync-narr-alternate │ ├── css │ │ └── blue.css │ ├── html │ │ ├── LITTLE-RED-RIDING-HOOD.html │ │ ├── TOADS-AND-DIAMONDS.html │ │ └── RUMPELSTILTZKIN.html │ ├── sync │ │ └── THE-MASTER-CAT;-OR,-PUSS-IN-BOOTS.json │ └── toc.html ├── abridged-blue-fairy-books │ ├── blue-fairy-audio │ │ ├── Blue_Fairy_Book_Audio.jpg │ │ ├── index.html │ │ └── blue.json │ ├── blue-fairy-syncnarr │ │ ├── Blue_Fairy_Book_SyncNarr.jpg │ │ ├── index.html │ │ ├── sync │ │ │ ├── THE-MASTER-CAT;-OR,-PUSS-IN-BOOTS.json │ │ │ ├── THE-HISTORY-OF-JACK-THE-GIANT-KILLER.json │ │ │ ├── RUMPELSTILTZKIN.json │ │ │ ├── CINDERELLA,-OR-THE-LITTLE-GLASS-SLIPPER.json │ │ │ └── HANSEL-AND-GRETTEL.json │ │ ├── blue.json │ │ └── html │ │ │ └── RUMPELSTILTZKIN.html │ └── blue-fairy-audio-html │ │ ├── Blue_Fairy_Book_Audio_Html.jpg │ │ ├── index.html │ │ ├── blue.json │ │ └── html │ │ └── RUMPELSTILTZKIN.html └── flatland │ ├── toc.html │ └── manifest.json ├── web ├── common │ ├── colors.css │ ├── utils.js │ ├── icon.svg │ └── localdata.js ├── player │ ├── css │ │ ├── base.css │ │ ├── pub-default.css │ │ ├── settings.css │ │ ├── player.css │ │ └── controls.css │ ├── events.js │ ├── nav.js │ ├── chapter.js │ ├── iframe.js │ ├── settings.html │ ├── audio.js │ ├── settings.js │ ├── index.html │ ├── narrator.js │ └── controls.js ├── library │ ├── style.css │ ├── index.html │ └── index.js └── test │ └── test-localdata.html ├── utils ├── package.json ├── utils.js ├── example │ └── syncpoints │ │ ├── THE-MASTER-CAT;-OR,-PUSS-IN-BOOTS.txt │ │ ├── THE-HISTORY-OF-JACK-THE-GIANT-KILLER.txt │ │ ├── RUMPELSTILTZKIN.txt │ │ ├── CINDERELLA,-OR-THE-LITTLE-GLASS-SLIPPER.txt │ │ └── HANSEL-AND-GRETTEL.txt ├── README.md ├── prepare-text.js ├── add-sync-narr-alternate.js └── add-html-alternate.js ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | out 4 | utils/schema 5 | .vscode/launch.json 6 | experiment 7 | -------------------------------------------------------------------------------- /content/epub/blue.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marisademeglio/worlds-best-audiobook/HEAD/content/epub/blue.epub -------------------------------------------------------------------------------- /content/screenreader-html-code/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marisademeglio/worlds-best-audiobook/HEAD/content/screenreader-html-code/cover.png -------------------------------------------------------------------------------- /content/audiobook/css/blue.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-size: 16pt; 3 | margin: 0; 4 | } 5 | 6 | h1 { 7 | font-size: 1.5em; 8 | margin: 0; 9 | } 10 | -------------------------------------------------------------------------------- /content/screenreader-html-code/radio-buttons.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marisademeglio/worlds-best-audiobook/HEAD/content/screenreader-html-code/radio-buttons.mp3 -------------------------------------------------------------------------------- /web/common/colors.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --bk: rgba(1,1,1,0.8); 3 | --text: white; 4 | --hltext: yellow; 5 | --hover: lightyellow; 6 | --workaroundbk: rgba(1,1,1,0); 7 | } -------------------------------------------------------------------------------- /content/The-Blue-Fairy-Book-with-sync-narr-alternate/css/blue.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-size: 16pt; 3 | margin: 0; 4 | } 5 | 6 | h1 { 7 | font-size: 1.5em; 8 | margin: 0; 9 | } 10 | -------------------------------------------------------------------------------- /content/abridged-blue-fairy-books/blue-fairy-audio/Blue_Fairy_Book_Audio.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marisademeglio/worlds-best-audiobook/HEAD/content/abridged-blue-fairy-books/blue-fairy-audio/Blue_Fairy_Book_Audio.jpg -------------------------------------------------------------------------------- /content/abridged-blue-fairy-books/blue-fairy-syncnarr/Blue_Fairy_Book_SyncNarr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marisademeglio/worlds-best-audiobook/HEAD/content/abridged-blue-fairy-books/blue-fairy-syncnarr/Blue_Fairy_Book_SyncNarr.jpg -------------------------------------------------------------------------------- /content/abridged-blue-fairy-books/blue-fairy-audio-html/Blue_Fairy_Book_Audio_Html.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marisademeglio/worlds-best-audiobook/HEAD/content/abridged-blue-fairy-books/blue-fairy-audio-html/Blue_Fairy_Book_Audio_Html.jpg -------------------------------------------------------------------------------- /web/player/css/base.css: -------------------------------------------------------------------------------- 1 | @import '../../common/colors.css'; 2 | 3 | body { 4 | font-family: Arial; 5 | background-color: var(--bk); 6 | color: var(--text); 7 | } 8 | 9 | a, a:visited { 10 | color: var(--text); 11 | padding-left: 1vh; 12 | text-decoration: none; 13 | } 14 | a:hover { 15 | text-decoration: underline !important; 16 | } 17 | .disabled { 18 | display: none; 19 | } 20 | button { 21 | cursor: pointer; 22 | background: transparent; 23 | border: none; 24 | color: var(--text); 25 | } 26 | -------------------------------------------------------------------------------- /utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "utils", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "add-html-alternate": "node add-html-alternate.js", 7 | "add-sync-narr-alternate": "node add-sync-narr-alternate.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "marisa.demeglio@gmail.com", 12 | "license": "ISC", 13 | "dependencies": { 14 | "commander": "^4.0.1", 15 | "djv": "^2.1.3-alpha.0", 16 | "extract-zip": "^1.6.7", 17 | "fs-extra": "^8.1.0", 18 | "jsdom": "^15.2.1", 19 | "tmp": "^0.1.0", 20 | "xmldom-alpha": "^0.1.28", 21 | "xpath": "0.0.27" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /utils/utils.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs-extra'); 3 | 4 | function stripPunctuation(str) { 5 | return str.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g,"").replace(/\s{2,}/g," ").trim(); 6 | } 7 | 8 | function writeOut(filename, contents, force=false) { 9 | let outpath = path.resolve(__dirname, 'out/', filename); 10 | if (fs.existsSync(outpath) && force || !fs.existsSync(outpath)) { 11 | fs.writeFileSync(outpath, contents); 12 | console.log(`Wrote ${outpath}`); 13 | } 14 | else { 15 | console.log(`ERROR: Cannot overwrite existing file of the same name:\n ${outpath}\nUse --force.\n`); 16 | } 17 | } 18 | 19 | 20 | module.exports = {stripPunctuation, writeOut} -------------------------------------------------------------------------------- /content/screenreader-html-code/radio-buttons.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --bk: rgba(1,1,1,0.8); 3 | --text: white; 4 | --hltext: yellow; 5 | --hover: lightyellow; 6 | } 7 | body { 8 | font-family: Arial; 9 | background-color: var(--bk); 10 | color: var(--text); 11 | } 12 | main { 13 | display: grid; 14 | grid-template-columns: 50% 50%; 15 | grid-gap: 2rem; 16 | } 17 | 18 | pre.language-markup { 19 | font-size: x-small; 20 | } 21 | 22 | form { 23 | line-height: 2; 24 | } 25 | 26 | .-active-element, .-active-element * { 27 | color: lightyellow !important; 28 | background-color: black !important; 29 | opacity: 0.9; 30 | transition: color .2s; 31 | border: none; 32 | border-radius: 3px; 33 | padding: 2px; 34 | text-shadow: none; 35 | } -------------------------------------------------------------------------------- /web/library/style.css: -------------------------------------------------------------------------------- 1 | @import "../common/colors.css"; 2 | 3 | body { 4 | font-family: Arial; 5 | margin: 2vw; 6 | background-color: var(--bk); 7 | color: var(--text); 8 | } 9 | 10 | ul { 11 | display: flex; 12 | list-style-type: none; 13 | padding: 0; 14 | margin: 0; 15 | gap: 2vw; 16 | } 17 | li { 18 | display: grid; 19 | grid-template-rows: auto 6vh; 20 | border-radius: 5px; 21 | padding: 1vh; 22 | max-width: 20rem; 23 | text-align: center; 24 | } 25 | li a, li a:visited { 26 | color: var(--text); 27 | text-decoration: none; 28 | } 29 | 30 | li a:last-child { 31 | margin-top: 3vh; 32 | } 33 | 34 | li:hover { 35 | box-shadow: 0 0 10px gray; 36 | } 37 | li img { 38 | max-width: 20rem; 39 | } 40 | @media (max-width: 768px) { 41 | ul { 42 | flex-direction: column; 43 | } 44 | } -------------------------------------------------------------------------------- /web/player/events.js: -------------------------------------------------------------------------------- 1 | // global event manager 2 | let eventHandlers = {}; 3 | 4 | function on(eventName, handler) { 5 | if (!eventHandlers[eventName]) { 6 | eventHandlers[eventName] = []; 7 | } 8 | eventHandlers[eventName].push(handler); 9 | } 10 | 11 | function off(eventName, handler) { 12 | let handlers = eventHandlers[eventName]; 13 | if (!handlers) return; 14 | for (let i = 0; i < handlers.length; i++) { 15 | if (handlers[i] === handler) { 16 | handlers.splice(i--, 1); 17 | } 18 | } 19 | } 20 | 21 | function trigger(eventName, ...args) { 22 | if (!eventHandlers[eventName]) { 23 | return; // no handlers for that event name 24 | } 25 | 26 | // call the handlers 27 | eventHandlers[eventName].forEach(handler => handler.apply(this, args)); 28 | } 29 | 30 | export { 31 | on, off, trigger 32 | }; -------------------------------------------------------------------------------- /web/player/css/pub-default.css: -------------------------------------------------------------------------------- 1 | @import 'base.css'; 2 | @import '../../common/colors.css'; 3 | 4 | .-active-element { 5 | color: var(--hltext); 6 | opacity: 0.9; 7 | transition: color .4s; 8 | } 9 | 10 | .-document-playing { 11 | 12 | } 13 | main { 14 | line-height: 1.5; 15 | } 16 | a, a:visited { 17 | color: var(--text); 18 | padding-left: 1vh; 19 | text-decoration: none; 20 | } 21 | a:hover { 22 | text-decoration: underline !important; 23 | } 24 | nav[role=doc-toc] ol { 25 | /* list-style-type: none; */ 26 | line-height: 5vh; 27 | padding-left: 0; 28 | color: var(--text); 29 | grid-column: 1; 30 | } 31 | 32 | nav[role=doc-toc] a.current, nav[role=doc-toc] a.current:visited { 33 | color: var(--hltext); 34 | opacity: 0.9; 35 | transition: color 1s; 36 | } 37 | nav[role=doc-toc] li { 38 | margin-top: 1rem; 39 | margin-bottom: 1rem; 40 | } -------------------------------------------------------------------------------- /web/library/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Audiobooks Library 5 | 6 | 7 | 8 | 9 | 10 | 11 | 20 | 21 | 22 |

Audiobooks Library

23 | 25 | 26 | -------------------------------------------------------------------------------- /web/common/utils.js: -------------------------------------------------------------------------------- 1 | async function fetchFile(file) { 2 | let data = await fetch(file); 3 | let text = await data.text(); 4 | return text; 5 | } 6 | 7 | function isImage(encodingFormat) { 8 | return [ 9 | 'image/jpeg', 10 | 'image/png', 11 | 'image/svg+xml', 12 | 'image/gif' 13 | ].includes(encodingFormat); 14 | } 15 | 16 | function isAudio(encodingFormat) { 17 | return [ 18 | 'audio/mpeg', 19 | 'audio/ogg', 20 | 'audio/mp-4' 21 | ].includes(encodingFormat); 22 | } 23 | function isText() { 24 | return true; 25 | } 26 | function isInViewport(elm, doc) { 27 | let bounding = elm.getBoundingClientRect(); 28 | return ( 29 | bounding.top >= 0 && 30 | bounding.left >= 0 && 31 | bounding.bottom <= (doc.defaultView.innerHeight || doc.documentElement.clientHeight) && 32 | bounding.right <= (doc.defaultView.innerWidth || doc.documentElement.clientWidth) 33 | ); 34 | } 35 | const secondsToHms = seconds => moment.utc(seconds * 1000).format('HH:mm:ss'); 36 | 37 | export { fetchFile, isImage, isAudio, isText, isInViewport, secondsToHms }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Marisa DeMeglio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /utils/example/syncpoints/THE-MASTER-CAT;-OR,-PUSS-IN-BOOTS.txt: -------------------------------------------------------------------------------- 1 | 18.973326 18.973326 2 | 22.324909 22.324909 3 | 45.068292 45.068292 4 | 56.743957 56.743957 5 | 63.051540 63.051540 6 | 76.092810 76.092810 7 | 119.508683 119.508683 8 | 141.959476 141.959476 9 | 153.102334 153.102334 10 | 158.302334 158.302334 11 | 180.257889 180.257889 12 | 195.940429 195.940429 13 | 205.148818 205.148818 14 | 214.393262 214.393262 15 | 218.437707 218.437707 16 | 245.015485 245.015485 17 | 255.332945 255.332945 18 | 287.193262 287.193262 19 | 296.107548 296.107548 20 | 301.367629 301.367629 21 | 307.984377 307.984377 22 | 313.857073 313.857073 23 | 318.912641 318.912641 24 | 327.648608 327.648608 25 | 334.323421 334.323421 26 | 352.547777 352.547777 27 | 375.098024 375.098024 28 | 379.802786 379.802786 29 | 390.863104 390.863104 30 | 397.136119 397.136119 31 | 418.290085 418.290085 32 | 432.958342 432.958342 33 | 437.130544 437.130544 34 | 445.421834 445.421834 35 | 457.352814 457.352814 36 | 461.312233 461.312233 37 | 473.057626 473.057626 38 | 502.704373 502.704373 39 | 507.326596 507.326596 40 | 515.495162 515.495162 41 | 521.110723 521.110723 42 | -------------------------------------------------------------------------------- /utils/example/syncpoints/THE-HISTORY-OF-JACK-THE-GIANT-KILLER.txt: -------------------------------------------------------------------------------- 1 | 19.085244 19.085244 2 | 21.361162 21.361162 3 | 36.628781 36.628781 4 | 48.767013 48.767013 5 | 62.991502 62.991502 6 | 67.258849 67.258849 7 | 92.673271 92.673271 8 | 105.001162 105.001162 9 | 121.975720 121.975720 10 | 132.975992 132.975992 11 | 143.881434 143.881434 12 | 160.761162 160.761162 13 | 183.235856 183.235856 14 | 202.296672 202.296672 15 | 221.167829 221.167829 16 | 231.504291 231.504291 17 | 234.064700 234.064700 18 | 236.435448 236.435448 19 | 240.133815 240.133815 20 | 259.099802 259.099802 21 | 271.807013 271.807013 22 | 288.497081 288.497081 23 | 298.549053 298.549053 24 | 316.756400 316.756400 25 | 337.713815 337.713815 26 | 351.369325 351.369325 27 | 367.395584 367.395584 28 | 387.878849 387.878849 29 | 408.646604 408.646604 30 | 430.457489 430.457489 31 | 443.923339 443.923339 32 | 453.501162 453.501162 33 | 464.027285 464.027285 34 | 485.079530 485.079530 35 | 495.890142 495.890142 36 | 511.157761 511.157761 37 | 513.338849 513.338849 38 | 516.563067 516.563067 39 | 524.908101 524.908101 40 | 548.520754 548.520754 41 | 559.995176 559.995176 42 | 577.159393 577.159393 43 | -------------------------------------------------------------------------------- /utils/example/syncpoints/RUMPELSTILTZKIN.txt: -------------------------------------------------------------------------------- 1 | 19.512190 19.512190 2 | 21.313959 21.313959 3 | 27.003755 27.003755 4 | 38.288517 38.288517 5 | 48.245660 48.245660 6 | 62.564980 62.564980 7 | 66.737497 66.737497 8 | 73.470422 73.470422 9 | 81.530966 81.530966 10 | 92.436408 92.436408 11 | 99.738313 99.738313 12 | 105.522939 105.522939 13 | 109.979946 109.979946 14 | 121.454367 121.454367 15 | 138.144435 138.144435 16 | 151.325796 151.325796 17 | 164.127837 164.127837 18 | 178.352327 178.352327 19 | 182.430014 182.430014 20 | 193.525116 193.525116 21 | 218.939537 218.939537 22 | 225.956952 225.956952 23 | 236.672735 236.672735 24 | 240.181442 240.181442 25 | 249.285116 249.285116 26 | 265.785524 265.785524 27 | 276.501306 276.501306 28 | 289.967156 289.967156 29 | 298.027701 298.027701 30 | 306.372735 306.372735 31 | 325.054231 325.054231 32 | 337.476952 337.476952 33 | 354.830830 354.830830 34 | 365.641442 365.641442 35 | 377.115864 377.115864 36 | 401.961306 401.961306 37 | 404.901034 404.901034 38 | 407.082122 407.082122 39 | 409.547701 409.547701 40 | 413.815048 413.815048 41 | 424.151510 424.151510 42 | 434.867293 434.867293 43 | 439.988109 439.988109 44 | 460.281714 460.281714 45 | -------------------------------------------------------------------------------- /content/abridged-blue-fairy-books/blue-fairy-syncnarr/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The Abridged Blue Fairy Book 5 | 6 | 7 | 8 | 25 | 26 | -------------------------------------------------------------------------------- /utils/README.md: -------------------------------------------------------------------------------- 1 | # utils 2 | 3 | ## add html alternate to audiobook 4 | 5 | `npm run add-html-alternate -- --epub ../content/epub/blue.epub --audiobook ../content/audiobook/blue.json --force` 6 | 7 | * Produces output (in this case) in: `utils/out/The-Blue-Fairy-Book-with-html-alternate` 8 | * Cleans up a gutenberg EPUB2 file, sorting by chapter. 9 | * Only brings over the chapters that appear in the audiobook (the names must match, disregarding case and punctuation) 10 | * Adds the cleaned-up HTML as `alternate`s for the audiobook reading order items 11 | 12 | ## add sync narr alternate to audiobook 13 | 14 | `npm run add-sync-narr-alternate -- --audiobook ./out/The-Blue-Fairy-Book-with-html-alternate/blue.json --sync ./example/syncpoints --force` 15 | 16 | * `--audiobook` is an audiobook with HTML alternates 17 | * `--syncpoints` is a directory containing audacity labels files. They must be named the same as the HTML alternate files, but with a `txt` extension. E.g. `THE-MASTER-CAT;-OR,-PUSS-IN-BOOTS.txt` 18 | 19 | This script creates a synchronized narration file for each chapter that has a corresponding `txt` file in the `syncpoints` directory. It then associates the synchronized narration JSON with the audiobook reading order item(s). 20 | 21 | It is not required to have syncpoints files for every chapter. If there are not syncpoints for a chapter, it remains as-is and still has an HTML alternate. 22 | -------------------------------------------------------------------------------- /content/abridged-blue-fairy-books/blue-fairy-audio/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The Abridged Blue Fairy Book (Audio) 5 | 6 | 7 | 8 | 9 | 10 | 27 | 28 | -------------------------------------------------------------------------------- /content/abridged-blue-fairy-books/blue-fairy-audio-html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The Abridged Blue Fairy Book 5 | 6 | 7 | 8 | 9 | 10 | 27 | 28 | -------------------------------------------------------------------------------- /web/player/css/settings.css: -------------------------------------------------------------------------------- 1 | body { 2 | grid-template-columns: 100% !important; 3 | } 4 | header { 5 | display: flex; 6 | } 7 | h2:not(:first-child){ 8 | margin-top: 3rem; 9 | } 10 | button { 11 | padding: 0.5rem; 12 | margin-top: 1rem; 13 | margin-right: 1rem; 14 | border: thin white solid; 15 | border-radius: 4px; 16 | } 17 | header p { 18 | align-self: center; 19 | margin-left: auto; 20 | } 21 | #text-settings { 22 | display: grid; 23 | grid-template-columns: 50% auto; 24 | } 25 | #sample-text { 26 | width: 20rem; 27 | /* border: thin solid white; */ 28 | /* padding: 1rem; */ 29 | } 30 | /* label[for=highlight] { 31 | visibility: hidden; 32 | } */ 33 | @media (max-width: 768px) { 34 | #sample-text { 35 | width: 70% 36 | } 37 | } 38 | 39 | table td:first-child { 40 | width: 30rem; 41 | } 42 | 43 | #custom-highlight { 44 | display: grid; 45 | grid-template-columns: 50% 50%; 46 | margin-top: 2rem; 47 | border: thin white solid; 48 | border-radius: 5px; 49 | cursor: auto; 50 | color: inherit; 51 | } 52 | #custom-highlight > fieldset { 53 | width: min-content; 54 | white-space: nowrap; 55 | border: none; 56 | padding: 1rem; 57 | display: flex; 58 | flex-direction: column; 59 | gap: 1rem; 60 | } 61 | #custom-highlight button { 62 | width: min-content; 63 | } 64 | 65 | #custom-highlight.disabled * { 66 | color: gray; 67 | border-color: gray; 68 | cursor: not-allowed; 69 | } 70 | #custom-highlight.disabled button { 71 | color: gray; 72 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The World's Best Audiobook 2 | 3 | Audiobook player + sample content 4 | 5 | [Live demo](https://marisademeglio.github.io/worlds-best-audiobook/web/library/) 6 | 7 | ## Scope 8 | 9 | Demonstrate playback of three types of audiobooks, all developed according to the [W3C Audiobooks](https://www.w3.org/TR/audiobooks/) standard. 10 | 11 | ## Goal 12 | 13 | Show how web standards make your audio books better. 14 | 15 | ## Features 16 | 17 | ### Audiobooks 18 | * Shows the cover and plays from beginning to end 19 | * Access to table of contents while listening 20 | * User controls for volume, rate, play/pause 21 | * Jump forward/back by 30 seconds 22 | * Remembers last-read position 23 | * Set bookmarks, named automatically by chapter + offset 24 | 25 | ### Audiobooks + HTML 26 | 27 | * All of the features above, PLUS 28 | * HTML page displayed during playback 29 | * Change the font size 30 | 31 | ### Audiobooks + Synchronized narration 32 | 33 | * All of the above, PLUS 34 | * Synchronized highlight of the HTML sentences or paragraphs during playback 35 | * Optional "Caption" mode 36 | * Instead of moving in 30 second increments, move by sentence 37 | * Click phrase to start playback 38 | * Change highlight color (settings) 39 | 40 | ## About this demo 41 | 42 | * Runs in a web browser 43 | * Employs principles of web accessibility 44 | * Built with HTML/CSS/Vanilla JS 45 | * Uses [audiobooks-js](https://marisademeglio.github.io/audiobooks-js) library 46 | 47 | ## Run it locally 48 | 49 | 1. Check out the code 50 | 2. Start a server: `npx http-serve -c-1` 51 | 3. Go to `http://localhost:8080/web/library/` 52 | 4. Click a title. 53 | 54 | -------------------------------------------------------------------------------- /web/library/index.js: -------------------------------------------------------------------------------- 1 | import { Manifest } from '../common/audiobooks.js'; 2 | import { isImage } from '../common/utils.js'; 3 | 4 | async function init(titles) { 5 | await populateTitles(titles); 6 | } 7 | 8 | // just grab the title and cover image for display 9 | async function populateTitles(titles) { 10 | console.log(titles); 11 | // titles are defined in the main library document 12 | let i; 13 | let titlesData = []; 14 | for (i=0; i { 37 | let titleListElm = document.querySelector("#titles"); 38 | let titleListItem = document.createElement("li"); 39 | titleListItem.innerHTML = ` 40 | 41 | Cover for ${titleData.title} 42 | 43 | 44 | ${titleData.title} 45 | 46 | `; 47 | titleListElm.appendChild(titleListItem); 48 | }); 49 | } 50 | 51 | export { 52 | init 53 | }; -------------------------------------------------------------------------------- /content/screenreader-html-code/publication.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": [ 3 | "https://schema.org", 4 | "https://www.w3.org/ns/pub-context" 5 | ], 6 | "conformsTo": "https://www.w3.org/TR/audiobooks/", 7 | "type": "Audiobook", 8 | "id": "screenreader-html-code", 9 | "url": "https://marisademeglio.github.io/worlds-best-audiobook/content/screenreader-html-code/", 10 | "name": "Example: Screenreader and HTML Code", 11 | "author": "Adrian Roselli", 12 | "readBy": "Various", 13 | "abridged": false, 14 | "accessMode": ["audio", "text"], 15 | "accessModeSufficient": [{ 16 | "type": "ItemList", 17 | "itemListElement": ["audio", "text"], 18 | "description": "audio and text" 19 | }], 20 | "accessibilityFeature": ["readingOrder", "unlocked", "tableOfContents"], 21 | "accessibilityHazard": "none", 22 | "accessibilitySummary": "Audio plus table of contents and synchronized text highlight", 23 | "dateModified": "2020-04-29", 24 | "datePublished": "1889", 25 | "inLanguage": "en", 26 | "readingProgression": "ltr", 27 | "duration": "PT3892S", 28 | "readingOrder": [ 29 | { 30 | "url": "radio-buttons.mp3", 31 | "encodingFormat": "audio/mpeg", 32 | "name": "Radio Buttons", 33 | "duration": "PT1170S", 34 | "alternate": { 35 | "type": "LinkedResource", 36 | "url": "radio-buttons.json", 37 | "encodingFormat": "application/vnd.syncnarr+json" 38 | } 39 | } 40 | ], 41 | "resources": [ 42 | { 43 | "url": "cover.png", 44 | "encodingFormat": "image/png", 45 | "rel": "cover", 46 | "name": "Cover" 47 | } 48 | ] 49 | } -------------------------------------------------------------------------------- /web/test/test-localdata.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test local data 5 | 6 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /content/abridged-blue-fairy-books/blue-fairy-syncnarr/sync/THE-MASTER-CAT;-OR,-PUSS-IN-BOOTS.json: -------------------------------------------------------------------------------- 1 | {"properties":{"text":"../html/THE-MASTER-CAT;-OR,-PUSS-IN-BOOTS.html","audio":"http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_14_lang_64kb.mp3","sync-media-css-class-active":"-active-element","sync-media-css-class-playing":"-document-playing"},"role":"document","narration":[{"text":"#sn-0","audio":"#t=18.97,22.32"},{"text":"#sn-1","audio":"#t=22.32,45.07"},{"text":"#sn-2","audio":"#t=45.07,56.74"},{"text":"#sn-3","audio":"#t=56.74,63.05"},{"text":"#sn-4","audio":"#t=63.05,76.09"},{"text":"#sn-5","audio":"#t=76.09,119.51"},{"text":"#sn-6","audio":"#t=119.51,141.96"},{"text":"#sn-7","audio":"#t=141.96,153.10"},{"text":"#sn-8","audio":"#t=153.10,158.30"},{"text":"#sn-9","audio":"#t=158.30,180.26"},{"text":"#sn-10","audio":"#t=180.26,195.94"},{"text":"#sn-11","audio":"#t=195.94,205.15"},{"text":"#sn-12","audio":"#t=205.15,214.39"},{"text":"#sn-13","audio":"#t=214.39,218.44"},{"text":"#sn-14","audio":"#t=218.44,245.02"},{"text":"#sn-15","audio":"#t=245.02,255.33"},{"text":"#sn-16","audio":"#t=255.33,287.19"},{"text":"#sn-17","audio":"#t=287.19,296.11"},{"text":"#sn-18","audio":"#t=296.11,301.37"},{"text":"#sn-19","audio":"#t=301.37,307.98"},{"text":"#sn-20","audio":"#t=307.98,313.86"},{"text":"#sn-21","audio":"#t=313.86,318.91"},{"text":"#sn-22","audio":"#t=318.91,327.65"},{"text":"#sn-23","audio":"#t=327.65,334.32"},{"text":"#sn-24","audio":"#t=334.32,352.55"},{"text":"#sn-25","audio":"#t=352.55,375.10"},{"text":"#sn-26","audio":"#t=375.10,379.80"},{"text":"#sn-27","audio":"#t=379.80,390.86"},{"text":"#sn-28","audio":"#t=390.86,397.14"},{"text":"#sn-29","audio":"#t=397.14,418.29"},{"text":"#sn-30","audio":"#t=418.29,432.96"},{"text":"#sn-31","audio":"#t=432.96,437.13"},{"text":"#sn-32","audio":"#t=437.13,445.42"},{"text":"#sn-33","audio":"#t=445.42,457.35"},{"text":"#sn-34","audio":"#t=457.35,461.31"},{"text":"#sn-35","audio":"#t=461.31,473.06"},{"text":"#sn-36","audio":"#t=473.06,502.70"},{"text":"#sn-37","audio":"#t=502.70,507.33"},{"text":"#sn-38","audio":"#t=507.33,515.50"},{"text":"#sn-39","audio":"#t=515.50,521.11"}]} -------------------------------------------------------------------------------- /utils/prepare-text.js: -------------------------------------------------------------------------------- 1 | // ingest a set of HTML documents 2 | // to any "leaf" element, add a class called "narrate" 3 | // this makes it easier for the narrator to identify discrete elements 4 | // when marking offsets 5 | 6 | const fs = require('fs-extra'); 7 | const path = require('path'); 8 | const program = require('commander'); 9 | const jsdom = require("jsdom"); 10 | const { JSDOM } = jsdom; 11 | const utils = require('./utils'); 12 | 13 | program.version('0.0.1'); 14 | program 15 | .requiredOption('-h, --html ', 'folder with html files'); 16 | program.parse(process.argv); 17 | 18 | let htmlPath = path.resolve(__dirname, program.html); 19 | let out = path.resolve(__dirname, 'out/prepare-text'); 20 | console.log(`Copying ${htmlPath} to ${out}`); 21 | fs.copySync(htmlPath, out); 22 | 23 | // list the html files in the directory 24 | let htmls = fs.readdirSync(out); 25 | htmls.map(htmlFile => { 26 | let html = fs.readFileSync(path.resolve(out, htmlFile)).toString(); 27 | const dom = new JSDOM(html); 28 | const doc = dom.window.document; 29 | let body = doc.querySelector("body"); 30 | addNarrationMarkers(body); 31 | 32 | utils.writeOut(path.resolve(out, htmlFile), dom.serialize(), true); 33 | }); 34 | 35 | console.log("Done"); 36 | 37 | function addNarrationMarkers(element) { 38 | console.log("Adding narration markers"); 39 | let children = Array.from(element.childNodes); 40 | // if all the children are type=1 and type=3 with only whitespace 41 | // recurse on the type 1 children and ignore the type 3s 42 | // else apply the class to this node 43 | let nonEmptyTextNodes = children.filter(child => 44 | child.nodeType === 3 && child.textContent.trim().length != 0); 45 | 46 | if (nonEmptyTextNodes.length > 0) { 47 | element.classList.add("narrate"); 48 | } 49 | else { 50 | children.map(child => { 51 | if (child.nodeType == 1) { 52 | addNarrationMarkers(child); 53 | } 54 | }); 55 | } 56 | 57 | 58 | // else if all its children are elements, then recursively call this function on each one 59 | } -------------------------------------------------------------------------------- /content/abridged-blue-fairy-books/blue-fairy-syncnarr/sync/THE-HISTORY-OF-JACK-THE-GIANT-KILLER.json: -------------------------------------------------------------------------------- 1 | {"properties":{"text":"../html/THE-HISTORY-OF-JACK-THE-GIANT-KILLER.html","audio":"http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_35_lang_64kb.mp3","sync-media-css-class-active":"-active-element","sync-media-css-class-playing":"-document-playing"},"role":"document","narration":[{"text":"#sn-0","audio":"#t=19.09,21.36"},{"text":"#sn-1","audio":"#t=21.36,36.63"},{"text":"#sn-2","audio":"#t=36.63,48.77"},{"text":"#sn-3","audio":"#t=48.77,62.99"},{"text":"#sn-4","audio":"#t=62.99,67.26"},{"text":"#sn-5","audio":"#t=67.26,92.67"},{"text":"#sn-6","audio":"#t=92.67,105.00"},{"text":"#sn-7","audio":"#t=105.00,121.98"},{"text":"#sn-8","audio":"#t=121.98,132.98"},{"text":"#sn-9","audio":"#t=132.98,143.88"},{"text":"#sn-10","audio":"#t=143.88,160.76"},{"text":"#sn-11","audio":"#t=160.76,183.24"},{"text":"#sn-12","audio":"#t=183.24,202.30"},{"text":"#sn-13","audio":"#t=202.30,221.17"},{"text":"#sn-14","audio":"#t=221.17,231.50"},{"text":"#sn-15","audio":"#t=231.50,234.06"},{"text":"#sn-16","audio":"#t=234.06,236.44"},{"text":"#sn-17","audio":"#t=236.44,240.13"},{"text":"#sn-18","audio":"#t=240.13,259.10"},{"text":"#sn-19","audio":"#t=259.10,271.81"},{"text":"#sn-20","audio":"#t=271.81,288.50"},{"text":"#sn-21","audio":"#t=288.50,298.55"},{"text":"#sn-22","audio":"#t=298.55,316.76"},{"text":"#sn-23","audio":"#t=316.76,337.71"},{"text":"#sn-24","audio":"#t=337.71,351.37"},{"text":"#sn-25","audio":"#t=351.37,367.40"},{"text":"#sn-26","audio":"#t=367.40,387.88"},{"text":"#sn-27","audio":"#t=387.88,408.65"},{"text":"#sn-28","audio":"#t=408.65,430.46"},{"text":"#sn-29","audio":"#t=430.46,443.92"},{"text":"#sn-30","audio":"#t=443.92,453.50"},{"text":"#sn-31","audio":"#t=453.50,464.03"},{"text":"#sn-32","audio":"#t=464.03,485.08"},{"text":"#sn-33","audio":"#t=485.08,495.89"},{"text":"#sn-34","audio":"#t=495.89,511.16"},{"text":"#sn-35","audio":"#t=511.16,513.34"},{"text":"#sn-36","audio":"#t=513.34,516.56"},{"text":"#sn-37","audio":"#t=516.56,524.91"},{"text":"#sn-38","audio":"#t=524.91,548.52"},{"text":"#sn-39","audio":"#t=548.52,560.00"},{"text":"#sn-40","audio":"#t=560.00,577.16"}]} -------------------------------------------------------------------------------- /content/abridged-blue-fairy-books/blue-fairy-syncnarr/sync/RUMPELSTILTZKIN.json: -------------------------------------------------------------------------------- 1 | {"properties":{"text":"../html/RUMPELSTILTZKIN.html","audio":"http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_10_lang_64kb.mp3","sync-media-css-class-active":"-active-element","sync-media-css-class-playing":"-document-playing"},"role":"document","narration":[{"text":"#sn-0","audio":"#t=19.51,21.31"},{"text":"#sn-1","audio":"#t=21.31,27.00"},{"text":"#sn-2","audio":"#t=27.00,38.29"},{"text":"#sn-3","audio":"#t=38.29,48.25"},{"text":"#sn-4","audio":"#t=48.25,62.56"},{"text":"#sn-5","audio":"#t=62.56,66.74"},{"text":"#sn-6","audio":"#t=66.74,73.47"},{"text":"#sn-7","audio":"#t=73.47,81.53"},{"text":"#sn-8","audio":"#t=81.53,92.44"},{"text":"#sn-9","audio":"#t=92.44,99.74"},{"text":"#sn-10","audio":"#t=99.74,105.52"},{"text":"#sn-11","audio":"#t=105.52,109.98"},{"text":"#sn-12","audio":"#t=109.98,121.45"},{"text":"#sn-13","audio":"#t=121.45,138.14"},{"text":"#sn-14","audio":"#t=138.14,151.33"},{"text":"#sn-15","audio":"#t=151.33,164.13"},{"text":"#sn-16","audio":"#t=164.13,178.35"},{"text":"#sn-17","audio":"#t=178.35,182.43"},{"text":"#sn-18","audio":"#t=182.43,193.53"},{"text":"#sn-19","audio":"#t=193.53,218.94"},{"text":"#sn-20","audio":"#t=218.94,225.96"},{"text":"#sn-21","audio":"#t=225.96,236.67"},{"text":"#sn-22","audio":"#t=236.67,240.18"},{"text":"#sn-23","audio":"#t=240.18,249.29"},{"text":"#sn-24","audio":"#t=249.29,265.79"},{"text":"#sn-25","audio":"#t=265.79,276.50"},{"text":"#sn-26","audio":"#t=276.50,289.97"},{"text":"#sn-27","audio":"#t=289.97,298.03"},{"text":"#sn-28","audio":"#t=298.03,306.37"},{"text":"#sn-29","audio":"#t=306.37,325.05"},{"text":"#sn-30","audio":"#t=325.05,337.48"},{"text":"#sn-31","audio":"#t=337.48,354.83"},{"text":"#sn-32","audio":"#t=354.83,365.64"},{"text":"#sn-33","audio":"#t=365.64,377.12"},{"text":"#sn-34","audio":"#t=377.12,401.96"},{"text":"#sn-35","audio":"#t=401.96,404.90"},{"text":"#sn-36","audio":"#t=404.90,407.08"},{"text":"#sn-37","audio":"#t=407.08,409.55"},{"text":"#sn-38","audio":"#t=409.55,413.82"},{"text":"#sn-39","audio":"#t=413.82,424.15"},{"text":"#sn-40","audio":"#t=424.15,434.87"},{"text":"#sn-41","audio":"#t=434.87,439.99"},{"text":"#sn-42","audio":"#t=439.99,460.28"}]} -------------------------------------------------------------------------------- /content/flatland/toc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Table of Contents 6 | 7 | 8 | 9 | 66 | 67 | -------------------------------------------------------------------------------- /web/player/nav.js: -------------------------------------------------------------------------------- 1 | import { fetchFile, isInViewport } from '../common/utils.js'; 2 | import { initIframe } from './iframe.js'; 3 | import * as Events from './events.js'; 4 | 5 | let base = ""; 6 | let onLoadContentListener = null; 7 | let tocdoc = null; 8 | let textColors = []; 9 | 10 | async function loadToc(manifest) { 11 | let toc = manifest.getToc(); 12 | if (manifest.hasHtmlToc()) { 13 | base = toc.url; 14 | await loadHtmlToc(toc.url); 15 | } 16 | else { 17 | base = manifest.base; 18 | loadGeneratedToc(toc); 19 | } 20 | } 21 | 22 | async function loadHtmlToc(url) { 23 | tocdoc = await initIframe(url, "#player-toc details div"); 24 | let navListElms = Array.from(tocdoc.querySelectorAll("[role=doc-toc] a")); 25 | navListElms.map(navListElm => { 26 | navListElm.addEventListener("click", (e) => { 27 | e.preventDefault(); 28 | Events.trigger('Nav.LoadContent', navListElm.getAttribute('href')); 29 | }); 30 | 31 | // save the colors so we can restore them 32 | textColors.push(navListElm.style.color); 33 | }); 34 | } 35 | 36 | 37 | function loadGeneratedToc(data, base) { 38 | base = base; 39 | tocdoc = document; 40 | let tocElm = document.querySelector("#player-toc details div") 41 | tocElm.innerHTML = ` 42 | `; 47 | let navListElms = Array.from(document.querySelectorAll("[role=doc-toc] li")); 48 | navListElms.map(navListElm => { 49 | navListElm.addEventListener("click", (e) => { 50 | e.preventDefault(); 51 | Events.trigger('Nav.LoadContent', 52 | navListElm.querySelector("a").getAttribute('href')); 53 | }); 54 | }); 55 | } 56 | 57 | function setCurrentTocItem(url) { 58 | let navListElms = Array.from(tocdoc.querySelectorAll("[role=doc-toc] a")); 59 | navListElms.map(elm => elm.classList.remove("current")); 60 | navListElms.map((elm, idx) => elm.style.color = textColors[idx]); 61 | let currentElm = navListElms.find(elm => new URL(elm.getAttribute('href'), base).href == url); 62 | if (currentElm) { 63 | currentElm.classList.add("current"); 64 | if (localStorage.getItem("highlight")) { 65 | currentElm.style.color = localStorage.getItem("highlight"); 66 | } 67 | if (!isInViewport(currentElm, tocdoc)) { 68 | currentElm.scrollIntoView(); 69 | } 70 | } 71 | } 72 | 73 | export { 74 | loadToc, 75 | setCurrentTocItem 76 | }; -------------------------------------------------------------------------------- /content/screenreader-html-code/radio-buttons.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "text": "radio-buttons.html", 4 | "audio": "radio-buttons.mp3", 5 | "sync-media-css-class-active": "-active-element", 6 | "sync-media-css-class-playing": "-document-playing" 7 | }, 8 | "narration": [ 9 | { 10 | "text": "#sn-500", 11 | "audio": "#t=0,1.4" 12 | }, 13 | { 14 | "text": "#sn-501", 15 | "audio": "#t=1.4,6.37" 16 | }, 17 | { 18 | "text": ["#sn-15", "#sn-300"], 19 | "audio": "#t=6.37,7.29" 20 | }, 21 | { 22 | "text": ["#sn-400", "#sn-301"], 23 | "audio": "#t=7.29,8.21" 24 | }, 25 | { 26 | "text": ["#sn-410", "#sn-304"], 27 | "audio": "#t=8.21,9.12" 28 | }, 29 | { 30 | "text": ["#sn-41", "#sn-43", "#sn-44", "#R1"], 31 | "audio": "#t=9.12,10.07" 32 | }, 33 | { 34 | "text": "#sn-14", 35 | "audio": "#t=10.07,10.69" 36 | }, 37 | { 38 | "text": ["#sn-41", "#sn-43","#sn-44", "#sn-48", "#sn-49"], 39 | "audio": "#t=10.69,11.71" 40 | }, 41 | { 42 | "text": ["#sn-420", "#sn-307"], 43 | "audio": "#t=11.71,12.56" 44 | }, 45 | { 46 | "text": ["#sn-86", "#sn-88", "#sn-89", "#R2"], 47 | "audio": "#t=12.56,13.66" 48 | }, 49 | { 50 | "text": "#sn-14", 51 | "audio": "#t=13.66,14.3" 52 | }, 53 | { 54 | "text": ["#sn-86", "#sn-88", "#sn-89", "#sn-93", "#sn-94"], 55 | "audio": "#t=14.3,15.04" 56 | }, 57 | { 58 | "text": ["#sn-430", "#sn-310"], 59 | "audio": "#t=15.04,15.82" 60 | }, 61 | { 62 | "text": ["#sn-131","#sn-133","#sn-134","#R3"], 63 | "audio": "#t=15.82,16.9," 64 | }, 65 | { 66 | "text": "#sn-14", 67 | "audio": "#t=16.9,17.52" 68 | }, 69 | { 70 | "text": ["#sn-131","#sn-133","#sn-134","#sn-138","#sn-139"], 71 | "audio": "#t=17.52,18.21" 72 | }, 73 | { 74 | "text": ["#sn-440", "#sn-313"], 75 | "audio": "#t=18.21,19.49" 76 | }, 77 | { 78 | "text": ["#sn-176", "#sn-178", "#sn-179", "#R4"], 79 | "audio": "#t=19.49,20.98" 80 | }, 81 | { 82 | "text": "#sn-14", 83 | "audio": "#t=20.98,21.53" 84 | }, 85 | { 86 | "text": ["#sn-176", "#sn-178", "#sn-179", "#sn-183", "#sn-184"], 87 | "audio": "#t=21.53,22.21" 88 | } 89 | ] 90 | } -------------------------------------------------------------------------------- /web/common/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 23 | 25 | 26 | 28 | image/svg+xml 29 | 31 | 32 | 33 | 34 | 35 | 37 | 58 | 61 | 64 | 69 | 74 | 75 | -------------------------------------------------------------------------------- /utils/example/syncpoints/CINDERELLA,-OR-THE-LITTLE-GLASS-SLIPPER.txt: -------------------------------------------------------------------------------- 1 | 28.511924 28.511924 2 | 31.736141 31.736141 3 | 40.555325 40.555325 4 | 50.607298 50.607298 5 | 64.167978 64.167978 6 | 73.176822 73.176822 7 | 83.797774 83.797774 8 | 96.884305 96.884305 9 | 120.022808 120.022808 10 | 130.454101 130.454101 11 | 151.506345 151.506345 12 | 164.308386 164.308386 13 | 171.799951 171.799951 14 | 179.196686 179.196686 15 | 191.809066 191.809066 16 | 205.938726 205.938726 17 | 212.956141 212.956141 18 | 217.796897 217.796897 19 | 230.409278 230.409278 20 | 244.444108 244.444108 21 | 265.401523 265.401523 22 | 268.910230 268.910230 23 | 273.462067 273.462067 24 | 281.048462 281.048462 25 | 289.583156 289.583156 26 | 298.497169 298.497169 27 | 305.135264 305.135264 28 | 317.463156 317.463156 29 | 331.023836 331.023836 30 | 337.092952 337.092952 31 | 346.860434 346.860434 32 | 357.196897 357.196897 33 | 362.033224 362.033224 34 | 370.188598 370.188598 35 | 374.361115 374.361115 36 | 377.869822 377.869822 37 | 391.904652 391.904652 38 | 409.258530 409.258530 39 | 427.086557 427.086557 40 | 439.414448 439.414448 41 | 442.259346 442.259346 42 | 451.173360 451.173360 43 | 456.389006 456.389006 44 | 462.837441 462.837441 45 | 482.562067 482.562067 46 | 491.286421 491.286421 47 | 512.338666 512.338666 48 | 515.657713 515.657713 49 | 523.623428 523.623428 50 | 531.114992 531.114992 51 | 545.813632 545.813632 52 | 553.115537 553.115537 53 | 564.210639 564.210639 54 | 570.279754 570.279754 55 | 583.555945 583.555945 56 | 596.073496 596.073496 57 | 614.565332 614.565332 58 | 634.669278 634.669278 59 | 639.600434 639.600434 60 | 654.014584 654.014584 61 | 671.558122 671.558122 62 | 685.972271 685.972271 63 | 696.972543 696.972543 64 | 715.274720 715.274720 65 | 732.818258 732.818258 66 | 749.034176 749.034176 67 | 761.362067 761.362067 68 | 780.233224 780.233224 69 | 788.293768 788.293768 70 | 801.949278 801.949278 71 | 825.846421 825.846421 72 | 841.872679 841.872679 73 | 852.114312 852.114312 74 | 868.045741 868.045741 . 75 | 877.918113 877.918113 76 | 895.082330 895.082330 77 | 909.022330 909.022330 78 | 913.289677 913.289677 79 | 919.643283 919.643283 80 | 933.962603 933.962603 81 | 937.376480 937.376480 82 | 941.074847 941.074847 83 | 954.066548 954.066548 84 | 965.540970 965.540970 85 | 983.084507 983.084507 86 | 996.076208 996.076208 87 | 1010.964507 1010.964507 88 | 1032.396072 1032.396072 89 | 1039.128997 1039.128997 90 | 1042.448045 1042.448045 91 | 1047.284371 1047.284371 92 | 1056.672535 1056.672535 93 | 1064.164099 1064.164099 94 | 1078.104099 1078.104099 95 | 1092.897569 1092.897569 96 | 1107.216888 1107.216888 97 | 1116.320562 1116.320562 98 | 1124.001786 1124.001786 99 | 1129.786412 1129.786412 100 | 1136.329677 1136.329677 101 | 1148.088589 1148.088589 102 | 1162.218249 1162.218249 103 | -------------------------------------------------------------------------------- /content/abridged-blue-fairy-books/blue-fairy-audio/blue.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": [ 3 | "https://schema.org", 4 | "https://www.w3.org/ns/pub-context" 5 | ], 6 | "conformsTo": "https://www.w3.org/TR/audiobooks/", 7 | "type": "Audiobook", 8 | "id": "blue-fairy-abridged-audio", 9 | "url": "https://marisademeglio.github.io/worlds-best-audiobook/content/blue-fairy-abridged/blue-fairy-audio/", 10 | "name": "The Abridged Blue Fairy Book", 11 | "author": "Andrew Lang", 12 | "readBy": "Various", 13 | "abridged": true, 14 | "accessMode": "audio", 15 | "accessModeSufficient": [{ 16 | "type": "ItemList", 17 | "itemListElement": ["audio"], 18 | "description": "audio" 19 | }], 20 | "accessibilityFeature": ["readingOrder", "unlocked", "tableOfContents"], 21 | "accessibilityHazard": "none", 22 | "accessibilitySummary": "Audio plus table of contents", 23 | "dateModified": "2020-04-29", 24 | "datePublished": "1889", 25 | "inLanguage": "en", 26 | "readingProgression": "ltr", 27 | "duration": "PT3892S", 28 | "readingOrder": [ 29 | { 30 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_07_lang_64kb.mp3", 31 | "encodingFormat": "audio/mpeg", 32 | "name": "Cinderella; or, The Little Glass Slipper", 33 | "duration": "PT1170S" 34 | }, 35 | { 36 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_10_lang_64kb.mp3", 37 | "encodingFormat": "audio/mpeg", 38 | "name": "Rumpelstiltzkin", 39 | "duration": "PT475S" 40 | }, 41 | { 42 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_14_lang_64kb.mp3", 43 | "encodingFormat": "audio/mpeg", 44 | "name": "The Master Cat; or, Puss in Boots", 45 | "duration": "PT532S" 46 | }, 47 | { 48 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_24_lang_64kb.mp3", 49 | "encodingFormat": "audio/mpeg", 50 | "name": "Hansel and Grettel", 51 | "duration": "PT1130S" 52 | }, 53 | { 54 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_35_lang_64kb.mp3", 55 | "encodingFormat": "audio/mpeg", 56 | "name": "The History of Jack the GiantKiller", 57 | "duration": "PT585S" 58 | } 59 | ], 60 | "resources": [ 61 | { 62 | "url": "Blue_Fairy_Book_Audio.jpg", 63 | "encodingFormat": "image/jpeg", 64 | "rel": "cover", 65 | "name": "Cover" 66 | }, 67 | { 68 | "url": "index.html", 69 | "encodingFormat": "text/html", 70 | "name": "Primary Entry Page", 71 | "rel": "contents" 72 | } 73 | ] 74 | } -------------------------------------------------------------------------------- /content/screenreader-html-code/prism.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.20.0 2 | https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */ 3 | /** 4 | * prism.js default theme for JavaScript, CSS and HTML 5 | * Based on dabblet (http://dabblet.com) 6 | * @author Lea Verou 7 | */ 8 | 9 | code[class*="language-"], 10 | pre[class*="language-"] { 11 | color: black; 12 | background: none; 13 | text-shadow: 0 1px white; 14 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 15 | font-size: 1em; 16 | text-align: left; 17 | white-space: pre; 18 | word-spacing: normal; 19 | word-break: normal; 20 | word-wrap: normal; 21 | line-height: 1.5; 22 | 23 | -moz-tab-size: 4; 24 | -o-tab-size: 4; 25 | tab-size: 4; 26 | 27 | -webkit-hyphens: none; 28 | -moz-hyphens: none; 29 | -ms-hyphens: none; 30 | hyphens: none; 31 | } 32 | 33 | pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, 34 | code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { 35 | text-shadow: none; 36 | background: #b3d4fc; 37 | } 38 | 39 | pre[class*="language-"]::selection, pre[class*="language-"] ::selection, 40 | code[class*="language-"]::selection, code[class*="language-"] ::selection { 41 | text-shadow: none; 42 | background: #b3d4fc; 43 | } 44 | 45 | @media print { 46 | code[class*="language-"], 47 | pre[class*="language-"] { 48 | text-shadow: none; 49 | } 50 | } 51 | 52 | /* Code blocks */ 53 | pre[class*="language-"] { 54 | padding: 1em; 55 | margin: .5em 0; 56 | overflow: auto; 57 | } 58 | 59 | :not(pre) > code[class*="language-"], 60 | pre[class*="language-"] { 61 | background: #f5f2f0; 62 | } 63 | 64 | /* Inline code */ 65 | :not(pre) > code[class*="language-"] { 66 | padding: .1em; 67 | border-radius: .3em; 68 | white-space: normal; 69 | } 70 | 71 | .token.comment, 72 | .token.prolog, 73 | .token.doctype, 74 | .token.cdata { 75 | color: slategray; 76 | } 77 | 78 | .token.punctuation { 79 | color: #999; 80 | } 81 | 82 | .token.namespace { 83 | opacity: .7; 84 | } 85 | 86 | .token.property, 87 | .token.tag, 88 | .token.boolean, 89 | .token.number, 90 | .token.constant, 91 | .token.symbol, 92 | .token.deleted { 93 | color: #905; 94 | } 95 | 96 | .token.selector, 97 | .token.attr-name, 98 | .token.string, 99 | .token.char, 100 | .token.builtin, 101 | .token.inserted { 102 | color: #690; 103 | } 104 | 105 | .token.operator, 106 | .token.entity, 107 | .token.url, 108 | .language-css .token.string, 109 | .style .token.string { 110 | color: #9a6e3a; 111 | background: hsla(0, 0%, 100%, .5); 112 | } 113 | 114 | .token.atrule, 115 | .token.attr-value, 116 | .token.keyword { 117 | color: #07a; 118 | } 119 | 120 | .token.function, 121 | .token.class-name { 122 | color: #DD4A68; 123 | } 124 | 125 | .token.regex, 126 | .token.important, 127 | .token.variable { 128 | color: #e90; 129 | } 130 | 131 | .token.important, 132 | .token.bold { 133 | font-weight: bold; 134 | } 135 | .token.italic { 136 | font-style: italic; 137 | } 138 | 139 | .token.entity { 140 | cursor: help; 141 | } 142 | 143 | -------------------------------------------------------------------------------- /web/player/chapter.js: -------------------------------------------------------------------------------- 1 | import * as Nav from './nav.js'; 2 | import * as Events from './events.js'; 3 | import * as Audio from './audio.js'; 4 | import * as Utils from '../common/utils.js'; 5 | import * as Narrator from './narrator.js'; 6 | import * as Controls from './controls.js'; 7 | import { initIframe } from './iframe.js'; 8 | 9 | // load content doc into the content pane 10 | async function play(manifest, autoplay, offset=0) { 11 | let readingOrderItem = manifest.getCurrentReadingOrderItem(); 12 | Nav.setCurrentTocItem(readingOrderItem.url); 13 | 14 | Events.off('Audio.ClipDone', onAudioClipDone); 15 | Events.off('Narrator.Done', onNarratorDone); 16 | 17 | if (Utils.isAudio(readingOrderItem.encodingFormat)) { 18 | if (readingOrderItem.hasOwnProperty('alternate')) { 19 | if (readingOrderItem.alternate[0].encodingFormat == "text/html") { 20 | log.info("Player: alternate is HTML"); 21 | await loadHtml(readingOrderItem.alternate[0].url); 22 | loadAudio(readingOrderItem.url, offset); 23 | } 24 | else if (readingOrderItem.alternate[0].encodingFormat == "application/vnd.syncnarr+json") { 25 | log.info("Player: alternate is sync narration"); 26 | await loadSyncNarration(readingOrderItem.alternate[0].url, autoplay, offset); 27 | } 28 | } 29 | else { 30 | log.info("Player: content is audio"); 31 | loadCover(manifest); 32 | loadAudio(readingOrderItem.url, autoplay, offset); 33 | } 34 | } 35 | } 36 | 37 | // just load the cover as the content 38 | function loadCover(manifest) { 39 | let contentElm = document.querySelector("#player-page"); 40 | let cover = manifest.getCover(); 41 | if (cover) { 42 | if (Utils.isImage(cover.encodingFormat)) { 43 | contentElm.innerHTML = `
Cover for ${manifest.getTitle()}
`; 44 | } 45 | else { 46 | // TODO load html cover 47 | } 48 | } 49 | } 50 | 51 | async function loadHtml(url) { 52 | await initIframe(url, "#player-page"); 53 | } 54 | 55 | function loadAudio(url, autoplay=true, offset=0) { 56 | Controls.showAudioControls(); 57 | Events.on('Audio.ClipDone', onAudioClipDone); 58 | Audio.playClip(url, autoplay, offset, -1, true); 59 | } 60 | 61 | async function loadSyncNarration(url, autoplay=true, offset=0) { 62 | Controls.showSyncNarrationControls(); 63 | Events.on('Narrator.Done', onNarratorDone); 64 | let data = await Utils.fetchFile(url); 65 | let syncnarrJson = JSON.parse(data); 66 | let htmlfile = new URL(syncnarrJson.properties.text, url).href; 67 | 68 | let iframeDoc = await initIframe(htmlfile, "#player-page"); 69 | Narrator.setHtmlDocument(iframeDoc); 70 | Narrator.loadJson(syncnarrJson, url, autoplay, offset); 71 | } 72 | 73 | function onAudioClipDone(src) { 74 | Events.trigger('Chapter.Done', src); 75 | } 76 | 77 | function onNarratorDone(src) { 78 | Events.trigger('Chapter.Done', src); 79 | } 80 | 81 | export { 82 | play 83 | }; 84 | -------------------------------------------------------------------------------- /web/player/iframe.js: -------------------------------------------------------------------------------- 1 | import * as Events from './events.js'; 2 | 3 | async function initIframe(url, parentSelector) { 4 | return new Promise((resolve, reject) => { 5 | let content = document.querySelector(parentSelector); 6 | content.innerHTML = ''; 7 | // disable the iframe parent element while we change the content and apply a stylesheet 8 | // but if it's already disabled, don't re-enable it at the end of this function 9 | // because it means we're in captions mode and we want it to stay disabled 10 | let wasAlreadyDisabled = content.classList.contains('disabled') && 11 | !document.querySelector("#player-captions").classList.contains('disabled'); 12 | if (!wasAlreadyDisabled) { 13 | content.classList.add('disabled'); 14 | } 15 | let iframe = document.createElement('iframe'); 16 | iframe.onload = () => { 17 | log.debug(`iframe loaded ${url}`); 18 | if (iframe.contentDocument) { 19 | if (iframe.contentDocument.styleSheets.length == 0) { 20 | log.info("Document has no styles -- applying default style") 21 | let iframeStyle = iframe.contentDocument.createElement('link'); 22 | iframeStyle.setAttribute('rel', 'stylesheet'); 23 | iframeStyle.setAttribute('href', new URL('css/pub-default.css', document.location.href)); 24 | let iframeHead = iframe.contentDocument.querySelector('head'); 25 | iframeHead.appendChild(iframeStyle); 26 | if (localStorage.getItem("fontsize")) { 27 | iframe.contentDocument.querySelector("body").style.fontSize = localStorage.getItem("fontsize"); 28 | } 29 | 30 | // a bit hacky but ensures we are only listening for clicks in the main text area 31 | // and not the TOC 32 | if (parentSelector.indexOf("player-page") != -1) { 33 | let allSyncedElms = Array.from(iframe.contentDocument.querySelectorAll("*[id]")); 34 | allSyncedElms.map(elm => { 35 | elm.addEventListener("click", e => { 36 | Events.trigger("Document.Click", e.target.getAttribute("id")); 37 | }); 38 | }); 39 | } 40 | 41 | resolve(iframe.contentDocument); 42 | } 43 | else { 44 | log.info("Document has default style, not modifying it"); 45 | resolve(iframe.contentDocument); 46 | } 47 | } 48 | else { 49 | log.warn("Can't access iframe content doc"); 50 | resolve(null); 51 | } 52 | // a short delay prevents the screen from flashing as it becomes un-disabled 53 | setTimeout(() => { 54 | if (!wasAlreadyDisabled) { 55 | content.classList.remove('disabled') 56 | } 57 | }, 300); 58 | }; 59 | iframe.setAttribute('src', url); 60 | content.appendChild(iframe); 61 | }); 62 | } 63 | 64 | export {initIframe}; -------------------------------------------------------------------------------- /web/player/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Audiobook Player Settings 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

Settings

16 |

Back to player

17 |
18 |
19 |

Text

20 |
21 |
22 |

Font size

23 | 32 |
33 | 34 | 35 |

Highlight

36 | 37 | 38 |
39 |
40 |
41 | 42 | 43 |
44 | 45 |
46 | 47 | 48 |
49 | 50 | 51 |
52 |

53 | Lorem ipsum dolor sit amet consectetur adipisicing elit. 54 | Ratione nisi, consequuntur saepe animi eum alias vero dolorem. 55 | Assumenda cum deserunt labore pariatur ut obcaecati, optio minima qui ducimus fuga laudantium? 56 |

57 | 58 |
59 | 60 |
61 | 62 |
63 | 64 |

Data

65 |

Bookmarks and last-read positions

66 | 67 | 68 | 69 |
70 | 71 |

72 | 73 |
74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /utils/example/syncpoints/HANSEL-AND-GRETTEL.txt: -------------------------------------------------------------------------------- 1 | 27.818034 27.818034 2 | 30.093952 30.093952 3 | 41.568374 41.568374 4 | 50.577217 50.577217 5 | 67.172456 67.172456 6 | 87.371231 87.371231 7 | 99.699122 99.699122 8 | 108.802796 108.802796 9 | 117.527149 117.527149 10 | 124.923884 124.923884 11 | 131.277490 131.277490 12 | 138.863884 138.863884 13 | 147.493408 147.493408 14 | 155.269462 155.269462 15 | 160.674768 160.674768 16 | 171.390551 171.390551 17 | 182.011503 182.011503 18 | 190.925517 190.925517 19 | 196.425653 196.425653 20 | 200.124020 200.124020 21 | 209.322524 209.322524 22 | 219.564156 219.564156 23 | 227.814360 227.814360 24 | 235.685245 235.685245 25 | 243.840619 243.840619 26 | 254.840891 254.840891 27 | 261.004837 261.004837 28 | 276.651775 276.651775 29 | 283.384700 283.384700 30 | 288.505517 288.505517 31 | 296.376401 296.376401 32 | 304.152456 304.152456 33 | 307.850823 307.850823 34 | 314.488918 314.488918 35 | 323.402932 323.402932 36 | 333.170415 333.170415 37 | 338.860211 338.860211 38 | 349.765653 349.765653 39 | 357.636537 357.636537 40 | 367.593680 367.593680 41 | 387.602796 387.602796 42 | 396.516809 396.516809 43 | 413.681027 413.681027 44 | 418.327694 418.327694 45 | 430.750415 430.750415 46 | 439.379939 439.379939 47 | 444.405925 444.405925 48 | 449.811231 449.811231 49 | 457.682115 457.682115 50 | 462.802932 462.802932 51 | 469.820347 469.820347 52 | 476.742932 476.742932 53 | 480.536129 480.536129 54 | 487.932864 487.932864 55 | 502.915993 502.915993 56 | 509.933408 509.933408 57 | 517.614632 517.614632 58 | 533.166741 533.166741 59 | 546.253272 546.253272 60 | 555.546605 555.546605 61 | 563.986469 563.986469 62 | 570.624564 570.624564 63 | 581.435177 581.435177 64 | 596.513136 596.513136 65 | 607.039258 607.039258 66 | 612.539394 612.539394 67 | 628.375993 628.375993 68 | 640.324564 640.324564 69 | 650.092047 650.092047 70 | 655.023204 655.023204 71 | 657.583612 657.583612 72 | 660.144020 660.144020 73 | 662.325109 662.325109 74 | 665.075177 665.075177 75 | 666.876945 666.876945 76 | 670.290823 670.290823 77 | 682.239394 682.239394 78 | 688.877490 688.877490 79 | 694.377626 694.377626 80 | 705.662388 705.662388 81 | 717.800619 717.800619 82 | 730.602660 730.602660 83 | 742.361571 742.361571 84 | 750.896265 750.896265 85 | 762.655177 762.655177 86 | 773.655449 773.655449 87 | 789.207558 789.207558 88 | 800.966469 800.966469 89 | 812.061571 812.061571 90 | 820.406605 820.406605 91 | 826.855041 826.855041 92 | 835.579394 835.579394 93 | 848.665925 848.665925 94 | 856.726469 856.726469 95 | 866.968102 866.968102 96 | 886.597898 886.597898 97 | 891.623884 891.623884 98 | 898.736129 898.736129 99 | 905.943204 905.943204 100 | 911.727830 911.727830 101 | 918.650415 918.650415 102 | 927.185109 927.185109 103 | 935.530143 935.530143 104 | 946.530415 946.530415 105 | 952.884020 952.884020 106 | 962.841163 962.841163 107 | 971.660347 971.660347 108 | 976.117354 976.117354 109 | 983.324428 983.324428 110 | 995.747149 995.747149 111 | 1008.454360 1008.454360 112 | 1014.428646 1014.428646 113 | 1018.601163 1018.601163 114 | 1025.144428 1025.144428 115 | 1037.661979 1037.661979 116 | 1041.170687 1041.170687 117 | 1044.015585 1044.015585 118 | 1046.575993 1046.575993 119 | 1049.515721 1049.515721 120 | 1056.817626 1056.817626 121 | 1064.214360 1064.214360 122 | 1078.154360 1078.154360 123 | 1083.844156 1083.844156 124 | 1090.671911 1090.671911 125 | 1100.913544 1100.913544 126 | 1107.361979 1107.361979 127 | 1116.844973 1116.844973 128 | -------------------------------------------------------------------------------- /web/player/css/player.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | body { 5 | height: 99%; 6 | display: grid; 7 | grid-template-areas: 8 | "header header header" 9 | "nav main aside" 10 | "footer footer footer"; 11 | grid-template-rows: min-content minmax(auto, 78%) min-content; 12 | grid-template-columns: 15% auto 20%; 13 | grid-gap: 1rem; 14 | overflow: hidden; 15 | } 16 | main { 17 | grid-area: main; 18 | overflow: scroll; 19 | } 20 | aside { 21 | grid-area: aside; 22 | overflow: scroll; 23 | } 24 | body > nav { 25 | grid-area: nav; 26 | } 27 | body > nav details { 28 | height: 100%; 29 | } 30 | body > nav details div { 31 | height: 100%; 32 | } 33 | 34 | body > nav div { 35 | overflow-y: scroll; 36 | } 37 | 38 | header { 39 | border-bottom: 2px solid gray; 40 | align-content: center; 41 | grid-area: header; 42 | display: grid; 43 | grid-template-areas: "title nav"; 44 | grid-template-columns: 80% auto; 45 | } 46 | header h1 { 47 | margin-top: 1rem; 48 | margin-bottom: 1rem; 49 | } 50 | header nav { 51 | grid-area: nav; 52 | display: grid; 53 | } 54 | header div#pub-info { 55 | grid-area: title; 56 | } 57 | header nav ul { 58 | list-style-type: none; 59 | display: flex; 60 | justify-content: space-evenly; 61 | padding: 0; 62 | align-items: center; 63 | } 64 | footer { 65 | grid-area: footer; 66 | } 67 | 68 | div#player-page { 69 | grid-column: 2/3; 70 | justify-self: center; 71 | height: 100%; 72 | } 73 | 74 | #cover-image-container { 75 | display: grid; 76 | } 77 | #cover-image-container img { 78 | justify-self: center; 79 | } 80 | iframe { 81 | height: 100%; 82 | background-color: white; 83 | border-width: 0; 84 | display: block; 85 | } 86 | #player-page iframe { 87 | width: 100%; 88 | } 89 | #bookmarks li { 90 | display: flex; 91 | margin-bottom: 1rem; 92 | } 93 | #bookmarks li button { 94 | margin-left: auto; 95 | } 96 | #bookmarks button { 97 | border: thin white solid; 98 | border-radius: 4px; 99 | padding: 0.25rem; 100 | } 101 | #player-captions { 102 | font-size: 3rem; 103 | text-align: center; 104 | margin-top: 2rem; 105 | color: var(--hltext); 106 | opacity: 0.9; 107 | } 108 | #edit-bookmarks { 109 | margin: auto; 110 | } 111 | #bookmarks nav { 112 | display: grid; 113 | } 114 | @media (max-width: 768px) { 115 | body { 116 | grid-template-areas: 117 | "header" 118 | "main" 119 | "footer" 120 | "nav" 121 | "aside"; 122 | grid-template-rows: min-content auto min-content min-content min-content; 123 | grid-template-columns: 0.9fr; 124 | grid-gap: 0.5rem 125 | 126 | } 127 | header h1 { 128 | font-size: medium; 129 | } 130 | header nav ul { 131 | flex-direction: column; 132 | font-size: medium; 133 | justify-self: right; 134 | margin-top: 1vh; 135 | } 136 | header nav li { 137 | line-height: 1.5; 138 | } 139 | #player-toc { 140 | margin-top: 1rem; 141 | margin-bottom: 1rem; 142 | font-weight: bold; 143 | } 144 | #edit-bookmarks { 145 | margin: 0; 146 | width: 3rem; 147 | } 148 | } 149 | 150 | @media (orientation: landscape) and (max-height: 500px) { 151 | header h1 { 152 | font-size: medium; 153 | } 154 | header nav { 155 | font-size: medium; 156 | } 157 | #player-toc iframe { 158 | width: 99%; 159 | } 160 | } -------------------------------------------------------------------------------- /content/flatland/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": [ 3 | "https://schema.org", 4 | "https://www.w3.org/ns/pub-context", 5 | {"language": "en", "direction": "ltr"} 6 | ], 7 | "conformsTo": "https://www.w3.org/TR/audiobooks/", 8 | "type": "Audiobook", 9 | "id": "https://librivox.org/flatland-a-romance-of-many-dimensions-by-edwin-abbott-abbott/", 10 | "url": "https://w3c.github.io/pub-manifest/experiments/audiobook/", 11 | "name": "Flatland: A Romance of Many Dimensions", 12 | "author": "Edwin Abbott Abbott", 13 | "readBy": "Ruth Golding", 14 | "publisher": "Librivox", 15 | "inLanguage": "en", 16 | "dateModified": "2019-11-14", 17 | "datePublished": "2008-10-12", 18 | "duration": "PT13774S", 19 | "license": "https://creativecommons.org/publicdomain/zero/1.0/", 20 | "abridged": false, 21 | "accessMode": "auditory", 22 | "accessModeSufficient": [{ 23 | "type": "ItemList", 24 | "itemListElement": ["auditory"], 25 | "description": "Audio" 26 | }], 27 | "accessibilityFeature": ["readingOrder", "unlocked"], 28 | "accessibilityHazard": "noSoundHazard", 29 | "accessibilitySummary": "This is just a test summary", 30 | "readingProgression": "ltr", 31 | "resources": [ 32 | { 33 | "rel": "cover", 34 | "url": "http://ia800704.us.archive.org/9/items/LibrivoxCdCoverArt12/Flatland_1109.jpg", 35 | "encodingFormat": "image/jpeg", 36 | "name": "Cover page with title and author" 37 | },{ 38 | "rel": "contents", 39 | "url": "toc.html", 40 | "encodingFormat": "text/html" 41 | },{ 42 | "rel": "accessibility-report", 43 | "url": "a11y.html", 44 | "encodingFormat": "text/html" 45 | },{ 46 | "rel": "privacy-policy,", 47 | "url": "privacy.html", 48 | "encodingFormat": "text/html" 49 | } 50 | ], 51 | 52 | "readingOrder": [ 53 | { 54 | "url": "http://www.archive.org/download/flatland_rg_librivox/flatland_1_abbott.mp3", 55 | "encodingFormat": "audio/mpeg", 56 | "duration": "PT1371S", 57 | "name": "Part 1, Sections 1 - 3" 58 | },{ 59 | "url": "http://www.archive.org/download/flatland_rg_librivox/flatland_2_abbott.mp3", 60 | "encodingFormat": "audio/mpeg", 61 | "duration": "PT1669S", 62 | "name": "Part 1, Sections 4 - 5" 63 | },{ 64 | "url": "http://www.archive.org/download/flatland_rg_librivox/flatland_3_abbott.mp3", 65 | "encodingFormat": "audio/mpeg", 66 | "duration": "PT1506S", 67 | "name": "Part 1, Sections 6 - 7" 68 | },{ 69 | "url": "http://www.archive.org/download/flatland_rg_librivox/flatland_4_abbott.mp3", 70 | "encodingFormat": "audio/mpeg", 71 | "duration": "PT1669S", 72 | "name": "Part 1, Sections 8 - 10" 73 | },{ 74 | "url": "http://www.archive.org/download/flatland_rg_librivox/flatland_5_abbott.mp3", 75 | "encodingFormat": "audio/mpeg", 76 | "duration": "PT1506S", 77 | "name": "Part 1, Sections 11 - 12" 78 | },{ 79 | "url": "http://www.archive.org/download/flatland_rg_librivox/flatland_6_abbott.mp3", 80 | "encodingFormat": "audio/mpeg", 81 | "duration": "PT1798S", 82 | "name": "Part 2, Sections 13 - 14" 83 | },{ 84 | "url": "http://www.archive.org/download/flatland_rg_librivox/flatland_7_abbott.mp3", 85 | "encodingFormat": "audio/mpeg", 86 | "duration": "PT1225S", 87 | "name": "Part 2, Sections 15 - 17" 88 | },{ 89 | "url": "http://www.archive.org/download/flatland_rg_librivox/flatland_8_abbott.mp3", 90 | "encodingFormat": "audio/mpeg", 91 | "duration": "PT1371S", 92 | "name": "Part 2, Sections 18 - 20" 93 | },{ 94 | "url": "http://www.archive.org/download/flatland_rg_librivox/flatland_9_abbott.mp3", 95 | "encodingFormat": "audio/mpeg", 96 | "duration": "PT1659S", 97 | "name": "Part 2, Sections 21 - 22" 98 | } 99 | ] 100 | } 101 | -------------------------------------------------------------------------------- /content/abridged-blue-fairy-books/blue-fairy-audio-html/blue.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": [ 3 | "https://schema.org", 4 | "https://www.w3.org/ns/pub-context" 5 | ], 6 | "conformsTo": "https://www.w3.org/TR/audiobooks/", 7 | "type": "Audiobook", 8 | "id": "blue-fairy-abridged-audio+html", 9 | "url": "https://marisademeglio.github.io/worlds-best-audiobook/content/blue-fairy-abridged/blue-fairy-audio+html/", 10 | "name": "The Abridged Blue Fairy Book (with Text)", 11 | "author": "Andrew Lang", 12 | "readBy": "Various", 13 | "abridged": true, 14 | "accessMode": ["audio", "text"], 15 | "accessModeSufficient": [{ 16 | "type": "ItemList", 17 | "itemListElement": ["audio", "text"], 18 | "description": "audio and text" 19 | }], 20 | "accessibilityFeature": ["readingOrder", "unlocked", "tableOfContents", "highContrastDisplay"], 21 | "accessibilityHazard": "none", 22 | "accessibilitySummary": "Audio plus table of contents and text for each chapter.", 23 | "dateModified": "2020-04-29", 24 | "datePublished": "1889", 25 | "inLanguage": "en", 26 | "readingProgression": "ltr", 27 | "duration": "PT3892S", 28 | "readingOrder": [ 29 | { 30 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_07_lang_64kb.mp3", 31 | "encodingFormat": "audio/mpeg", 32 | "name": "Cinderella; or, The Little Glass Slipper", 33 | "duration": "PT1170S", 34 | "alternate": { 35 | "encodingFormat": "text/html", 36 | "url": "html/CINDERELLA,-OR-THE-LITTLE-GLASS-SLIPPER.html", 37 | "type": "LinkedResource" 38 | } 39 | }, 40 | { 41 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_10_lang_64kb.mp3", 42 | "encodingFormat": "audio/mpeg", 43 | "name": "Rumpelstiltzkin", 44 | "duration": "PT475S", 45 | "alternate": { 46 | "encodingFormat": "text/html", 47 | "url": "html/RUMPELSTILTZKIN.html", 48 | "type": "LinkedResource" 49 | } 50 | }, 51 | { 52 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_14_lang_64kb.mp3", 53 | "encodingFormat": "audio/mpeg", 54 | "name": "The Master Cat; or, Puss in Boots", 55 | "duration": "PT532S", 56 | "alternate": { 57 | "encodingFormat": "text/html", 58 | "url": "html/THE-MASTER-CAT;-OR,-PUSS-IN-BOOTS.html", 59 | "type": "LinkedResource" 60 | } 61 | }, 62 | { 63 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_24_lang_64kb.mp3", 64 | "encodingFormat": "audio/mpeg", 65 | "name": "Hansel and Grettel", 66 | "duration": "PT1130S", 67 | "alternate": { 68 | "encodingFormat": "text/html", 69 | "url": "html/HANSEL-AND-GRETTEL.html", 70 | "type": "LinkedResource" 71 | } 72 | }, 73 | { 74 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_35_lang_64kb.mp3", 75 | "encodingFormat": "audio/mpeg", 76 | "name": "The History of Jack the GiantKiller", 77 | "duration": "PT585S", 78 | "alternate": { 79 | "encodingFormat": "text/html", 80 | "url": "html/THE-HISTORY-OF-JACK-THE-GIANT-KILLER.html", 81 | "type": "LinkedResource" 82 | } 83 | } 84 | ], 85 | "resources": [ 86 | { 87 | "url": "Blue_Fairy_Book_Audio_Html.jpg", 88 | "encodingFormat": "image/jpeg", 89 | "rel": "cover", 90 | "name": "Cover" 91 | }, 92 | { 93 | "url": "index.html", 94 | "encodingFormat": "text/html", 95 | "name": "Primary Entry Page", 96 | "rel": "contents" 97 | } 98 | ] 99 | } -------------------------------------------------------------------------------- /content/abridged-blue-fairy-books/blue-fairy-syncnarr/blue.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": [ 3 | "https://schema.org", 4 | "https://www.w3.org/ns/pub-context" 5 | ], 6 | "conformsTo": "https://www.w3.org/TR/audiobooks/", 7 | "type": "Audiobook", 8 | "id": "blue-fairy-abridged-syncnarr", 9 | "url": "https://marisademeglio.github.io/worlds-best-audiobook/content/blue-fairy-abridged/blue-fairy-syncnarr/", 10 | "name": "The Abridged Blue Fairy Book (with Synchronized Narration)", 11 | "author": "Andrew Lang", 12 | "readBy": "Various", 13 | "abridged": true, 14 | "accessMode": ["audio", "text"], 15 | "accessModeSufficient": [{ 16 | "type": "ItemList", 17 | "itemListElement": ["audio", "text"], 18 | "description": "audio and text" 19 | }], 20 | "accessibilityFeature": ["readingOrder", "unlocked", "tableOfContents", "highContrastDisplay"], 21 | "accessibilityHazard": "none", 22 | "accessibilitySummary": "Audio plus table of contents and synchronized text highlight for each chapter.", 23 | "dateModified": "2020-04-29", 24 | "datePublished": "1889", 25 | "inLanguage": "en", 26 | "readingProgression": "ltr", 27 | "duration": "PT3892S", 28 | "readingOrder": [ 29 | { 30 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_07_lang_64kb.mp3", 31 | "encodingFormat": "audio/mpeg", 32 | "name": "Cinderella; or, The Little Glass Slipper", 33 | "duration": "PT1170S", 34 | "alternate": { 35 | "type": "LinkedResource", 36 | "url": "sync/CINDERELLA,-OR-THE-LITTLE-GLASS-SLIPPER.json", 37 | "encodingFormat": "application/vnd.syncnarr+json" 38 | } 39 | }, 40 | { 41 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_10_lang_64kb.mp3", 42 | "encodingFormat": "audio/mpeg", 43 | "name": "Rumpelstiltzkin", 44 | "duration": "PT475S", 45 | "alternate": { 46 | "type": "LinkedResource", 47 | "url": "sync/RUMPELSTILTZKIN.json", 48 | "encodingFormat": "application/vnd.syncnarr+json" 49 | } 50 | }, 51 | { 52 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_14_lang_64kb.mp3", 53 | "encodingFormat": "audio/mpeg", 54 | "name": "The Master Cat; or, Puss in Boots", 55 | "duration": "PT532S", 56 | "alternate": { 57 | "type": "LinkedResource", 58 | "url": "sync/THE-MASTER-CAT;-OR,-PUSS-IN-BOOTS.json", 59 | "encodingFormat": "application/vnd.syncnarr+json" 60 | } 61 | }, 62 | { 63 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_24_lang_64kb.mp3", 64 | "encodingFormat": "audio/mpeg", 65 | "name": "Hansel and Grettel", 66 | "duration": "PT1130S", 67 | "alternate": { 68 | "type": "LinkedResource", 69 | "url": "sync/HANSEL-AND-GRETTEL.json", 70 | "encodingFormat": "application/vnd.syncnarr+json" 71 | } 72 | }, 73 | { 74 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_35_lang_64kb.mp3", 75 | "encodingFormat": "audio/mpeg", 76 | "name": "The History of Jack the GiantKiller", 77 | "duration": "PT585S", 78 | "alternate": { 79 | "type": "LinkedResource", 80 | "url": "sync/THE-HISTORY-OF-JACK-THE-GIANT-KILLER.json", 81 | "encodingFormat": "application/vnd.syncnarr+json" 82 | } 83 | } 84 | ], 85 | "resources": [ 86 | { 87 | "url": "Blue_Fairy_Book_SyncNarr.jpg", 88 | "encodingFormat": "image/jpeg", 89 | "rel": "cover", 90 | "name": "Cover" 91 | }, 92 | { 93 | "url": "index.html", 94 | "encodingFormat": "text/html", 95 | "name": "Primary Entry Page", 96 | "rel": "contents" 97 | } 98 | ] 99 | } -------------------------------------------------------------------------------- /content/The-Blue-Fairy-Book-with-sync-narr-alternate/html/LITTLE-RED-RIDING-HOOD.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LITTLE RED RIDING HOOD 6 | 7 | 8 | 9 |

LITTLE RED RIDING HOOD

10 |

Once upon a time there lived in a certain village a little country girl, the prettiest creature was ever seen. 11 | Her mother was excessively fond of her; and her grandmother doted on her still more. This good woman had made 12 | for her a little red riding-hood; which became the girl so extremely well that everybody called her Little Red 13 | Riding-Hood.

14 |

One day her mother, having made some custards, said to her:

15 |

“Go, my dear, and see how thy grandmamma does, for I hear she has been very ill; carry her a custard, and this 16 | little pot of butter.”

17 |

Little Red Riding-Hood set out immediately to go to her grandmother, who lived in another village.

18 |

As she was going through the wood, she met with Gaffer Wolf, who had a very great mind to eat her up, but he 19 | dared not, because of some faggot-makers hard by in the forest. He asked her whither she was going. The poor 20 | child, who did not know that it was dangerous to stay and hear a wolf talk, said to him:

21 |

“I am going to see my grandmamma and carry her a custard and a little pot of butter from my mamma.”

22 |

“Does she live far off?” said the Wolf.

23 |

“Oh! ay,” answered Little Red Riding-Hood; “it is beyond that mill you see there, at the first house in the 24 | village.”

25 |

“Well,” said the Wolf, “and I’ll go and see her too. I’ll go this way and you go that, and we shall see who will 26 | be there soonest.”

27 |

The Wolf began to run as fast as he could, taking the nearest way, and the little girl went by that farthest 28 | about, diverting herself in gathering nuts, running after butterflies, and making nosegays of such little 29 | flowers as she met with. The Wolf was not long before he got to the old woman’s house. He knocked at the 30 | door—tap, tap.

31 |

“Who’s there?”

32 |

“Your grandchild, Little Red Riding-Hood,” replied the Wolf, counterfeiting her voice; “who has brought you a 33 | custard and a little pot of butter sent you by mamma.”

34 |

The good grandmother, who was in bed, because she was somewhat ill, cried out:

35 |

“Pull the bobbin, and the latch will go up.”

36 |

The Wolf pulled the bobbin, and the door opened, and then presently he fell upon the good woman and ate her up in 37 | a moment, for it was above three days that he had not touched a bit. He then shut the door and went into the 38 | grandmother’s bed, expecting Little Red Riding-Hood, who came some time afterward and knocked at the door—tap, 39 | tap.

40 |

“Who’s there?”

41 |

Little Red Riding-Hood, hearing the big voice of the Wolf, was at first afraid; but believing her grandmother had 42 | got a cold and was hoarse, answered:

43 |

“‘Tis your grandchild, Little Red Riding-Hood, who has brought you a custard and a little pot of butter mamma 44 | sends you.”

45 |

The Wolf cried out to her, softening his voice as much as he could:

46 |

“Pull the bobbin, and the latch will go up.”

47 |

Little Red Riding-Hood pulled the bobbin, and the door opened.

48 |

The Wolf, seeing her come in, said to her, hiding himself under the bed-clothes:

49 |

“Put the custard and the little pot of butter upon the stool, and come and lie down with me.”

50 |

Little Red Riding-Hood undressed herself and went into bed, where, being greatly amazed to see how her 51 | grandmother looked in her night-clothes, she said to her:

52 |

“Grandmamma, what great arms you have got!”

53 |

“That is the better to hug thee, my dear.”

54 |

“Grandmamma, what great legs you have got!”

55 |

“That is to run the better, my child.”

56 |

“Grandmamma, what great ears you have got!”

57 |

“That is to hear the better, my child.”

58 |

“Grandmamma, what great eyes you have got!”

59 |

“It is to see the better, my child.”

60 |

“Grandmamma, what great teeth you have got!”

61 |

“That is to eat thee up.”

62 |

And, saying these words, this wicked wolf fell upon Little Red Riding-Hood, and ate her all up.

63 | 64 | 65 | -------------------------------------------------------------------------------- /web/common/localdata.js: -------------------------------------------------------------------------------- 1 | import { openDB, deleteDB } from 'https://unpkg.com/idb?module'; 2 | 3 | const dbname = "webplayer"; 4 | const dbversion = 1; 5 | var db = null; 6 | 7 | async function initdb() { 8 | db = await openDB(dbname, dbversion, { 9 | upgrade(db) { 10 | // Create a store of objects 11 | const store = db.createObjectStore('positions', { 12 | // The 'id' property of the object will be the key. 13 | keyPath: 'id', 14 | // If it isn't explicitly set, create a value by auto incrementing. 15 | autoIncrement: true, 16 | }); 17 | // Create an index on the 'pubid' property of the objects. 18 | store.createIndex('pubid', 'pubid'); 19 | }, 20 | }); 21 | } 22 | 23 | // tends to be blocked... 24 | async function deletedb() { 25 | await deleteDB(dbname, { 26 | blocked() { 27 | log.error("IndexedDB error: Delete DB blocked"); 28 | }, 29 | }); 30 | db = null; 31 | } 32 | 33 | async function deleteAll() { 34 | let records = await db.getAll('positions'); 35 | let i; 36 | if (records) { 37 | for (i=0; i pub.pubid === pubid)) { 45 | await db.add('positions', {pubid, title, type: "publication"}); 46 | } 47 | } 48 | async function getPublications() { 49 | let alldata = await db.getAll('positions'); 50 | return alldata.filter(data => data.type === "publication"); 51 | } 52 | async function removePosition(id) { 53 | await db.delete('positions', id); 54 | } 55 | async function getPosition(id) { 56 | await db.get('positions', id, 'id'); 57 | } 58 | async function getPositions(pubid) { 59 | return await db.getAllFromIndex('positions', 'pubid', pubid); 60 | } 61 | // just copy from the most recent last read position 62 | // which is constantly updated 63 | async function addBookmarkAtCurrentPosition(pubid) { 64 | let lastRead = await getLastRead(pubid); 65 | let bmk = { 66 | pubid: lastRead.pubid, 67 | readingOrderItem: lastRead.readingOrderItem, 68 | offset: lastRead.offset, 69 | label: lastRead.label, 70 | type: "bookmark" 71 | }; 72 | await addBookmark(bmk); 73 | } 74 | async function addBookmark(data) { 75 | await db.add('positions', {...data, type: 'bookmark'}); 76 | } 77 | async function getBookmarks(pubid) { 78 | let records = await db.getAllFromIndex('positions', 'pubid', pubid); 79 | if (records) { 80 | return records.filter(r => r.type === 'bookmark'); 81 | } 82 | else { 83 | return []; 84 | } 85 | } 86 | // nicer name 87 | async function deleteBookmark(id) { 88 | await removePosition(parseInt(id)); 89 | } 90 | 91 | async function getLastRead(pubid) { 92 | let records = await db.getAllFromIndex('positions', 'pubid', pubid); 93 | // there is a way to query the db on multiple indices but the documentation is bad 94 | // and the search capabilities of indexeddb are generally recognized as limited 95 | // there won't be that many records so this is easier both architecturally and considering that 96 | // indexeddb will likely get better in this regard in the future, given the number of complaints 97 | if (records) { 98 | return records.find(r=>r.type === 'last'); 99 | } 100 | else { 101 | return null; 102 | } 103 | } 104 | 105 | async function updateLastRead(data) { 106 | let position = await getLastRead(data.pubid); 107 | // update if exists 108 | if (position) { 109 | let newPosition = { ...position, ...data }; 110 | await db.put('positions', newPosition); 111 | } 112 | // else add a new record 113 | else { 114 | await db.add('positions', {...data, type: 'last'}); 115 | } 116 | } 117 | 118 | export { 119 | initdb, deletedb, deleteAll, 120 | addBookmark, addBookmarkAtCurrentPosition, getBookmarks, 121 | getPosition, removePosition, getPositions, 122 | getLastRead, updateLastRead, deleteBookmark, 123 | addPublication, getPublications }; 124 | 125 | /* position data: 126 | 127 | Required for submitting 128 | { 129 | pubid: "PublicationID", 130 | readingOrderItem: "relativeURL/item.mp3", 131 | label: label text, 132 | offset: 400ms 133 | } 134 | 135 | Additional fields added by these functions: 136 | { 137 | ... 138 | type: "last" | "bookmark" | "publication", 139 | id: ID 140 | } 141 | 142 | */ -------------------------------------------------------------------------------- /content/abridged-blue-fairy-books/blue-fairy-syncnarr/sync/CINDERELLA,-OR-THE-LITTLE-GLASS-SLIPPER.json: -------------------------------------------------------------------------------- 1 | {"properties":{"text":"../html/CINDERELLA,-OR-THE-LITTLE-GLASS-SLIPPER.html","audio":"http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_07_lang_64kb.mp3","sync-media-css-class-active":"-active-element","sync-media-css-class-playing":"-document-playing"},"role":"document","narration":[{"text":"#sn-0","audio":"#t=28.51,31.74"},{"text":"#sn-1","audio":"#t=31.74,40.56"},{"text":"#sn-2","audio":"#t=40.56,50.61"},{"text":"#sn-3","audio":"#t=50.61,64.17"},{"text":"#sn-4","audio":"#t=64.17,73.18"},{"text":"#sn-5","audio":"#t=73.18,83.80"},{"text":"#sn-6","audio":"#t=83.80,96.88"},{"text":"#sn-7","audio":"#t=96.88,120.02"},{"text":"#sn-8","audio":"#t=120.02,130.45"},{"text":"#sn-9","audio":"#t=130.45,151.51"},{"text":"#sn-10","audio":"#t=151.51,164.31"},{"text":"#sn-11","audio":"#t=164.31,171.80"},{"text":"#sn-12","audio":"#t=171.80,179.20"},{"text":"#sn-13","audio":"#t=179.20,191.81"},{"text":"#sn-14","audio":"#t=191.81,205.94"},{"text":"#sn-15","audio":"#t=205.94,212.96"},{"text":"#sn-16","audio":"#t=212.96,217.80"},{"text":"#sn-17","audio":"#t=217.80,230.41"},{"text":"#sn-18","audio":"#t=230.41,244.44"},{"text":"#sn-19","audio":"#t=244.44,265.40"},{"text":"#sn-20","audio":"#t=265.40,268.91"},{"text":"#sn-21","audio":"#t=268.91,273.46"},{"text":"#sn-22","audio":"#t=273.46,281.05"},{"text":"#sn-23","audio":"#t=281.05,289.58"},{"text":"#sn-24","audio":"#t=289.58,298.50"},{"text":"#sn-25","audio":"#t=298.50,305.14"},{"text":"#sn-26","audio":"#t=305.14,317.46"},{"text":"#sn-27","audio":"#t=317.46,331.02"},{"text":"#sn-28","audio":"#t=331.02,337.09"},{"text":"#sn-29","audio":"#t=337.09,346.86"},{"text":"#sn-30","audio":"#t=346.86,357.20"},{"text":"#sn-31","audio":"#t=357.20,362.03"},{"text":"#sn-32","audio":"#t=362.03,370.19"},{"text":"#sn-33","audio":"#t=370.19,374.36"},{"text":"#sn-34","audio":"#t=374.36,377.87"},{"text":"#sn-35","audio":"#t=377.87,391.90"},{"text":"#sn-36","audio":"#t=391.90,409.26"},{"text":"#sn-37","audio":"#t=409.26,427.09"},{"text":"#sn-38","audio":"#t=427.09,439.41"},{"text":"#sn-39","audio":"#t=439.41,442.26"},{"text":"#sn-40","audio":"#t=442.26,451.17"},{"text":"#sn-41","audio":"#t=451.17,456.39"},{"text":"#sn-42","audio":"#t=456.39,462.84"},{"text":"#sn-43","audio":"#t=462.84,482.56"},{"text":"#sn-44","audio":"#t=482.56,491.29"},{"text":"#sn-45","audio":"#t=491.29,512.34"},{"text":"#sn-46","audio":"#t=512.34,515.66"},{"text":"#sn-47","audio":"#t=515.66,523.62"},{"text":"#sn-48","audio":"#t=523.62,531.11"},{"text":"#sn-49","audio":"#t=531.11,545.81"},{"text":"#sn-50","audio":"#t=545.81,553.12"},{"text":"#sn-51","audio":"#t=553.12,564.21"},{"text":"#sn-52","audio":"#t=564.21,570.28"},{"text":"#sn-53","audio":"#t=570.28,583.56"},{"text":"#sn-54","audio":"#t=583.56,596.07"},{"text":"#sn-55","audio":"#t=596.07,614.57"},{"text":"#sn-56","audio":"#t=614.57,634.67"},{"text":"#sn-57","audio":"#t=634.67,639.60"},{"text":"#sn-58","audio":"#t=639.60,654.01"},{"text":"#sn-59","audio":"#t=654.01,671.56"},{"text":"#sn-60","audio":"#t=671.56,685.97"},{"text":"#sn-61","audio":"#t=685.97,696.97"},{"text":"#sn-62","audio":"#t=696.97,715.27"},{"text":"#sn-63","audio":"#t=715.27,732.82"},{"text":"#sn-64","audio":"#t=732.82,749.03"},{"text":"#sn-65","audio":"#t=749.03,761.36"},{"text":"#sn-66","audio":"#t=761.36,780.23"},{"text":"#sn-67","audio":"#t=780.23,788.29"},{"text":"#sn-68","audio":"#t=788.29,801.95"},{"text":"#sn-69","audio":"#t=801.95,825.85"},{"text":"#sn-70","audio":"#t=825.85,841.87"},{"text":"#sn-71","audio":"#t=841.87,852.11"},{"text":"#sn-72","audio":"#t=852.11,868.05"},{"text":"#sn-73","audio":"#t=868.05,877.92"},{"text":"#sn-74","audio":"#t=877.92,895.08"},{"text":"#sn-75","audio":"#t=895.08,909.02"},{"text":"#sn-76","audio":"#t=909.02,913.29"},{"text":"#sn-77","audio":"#t=913.29,919.64"},{"text":"#sn-78","audio":"#t=919.64,933.96"},{"text":"#sn-79","audio":"#t=933.96,937.38"},{"text":"#sn-80","audio":"#t=937.38,941.07"},{"text":"#sn-81","audio":"#t=941.07,954.07"},{"text":"#sn-82","audio":"#t=954.07,965.54"},{"text":"#sn-83","audio":"#t=965.54,983.08"},{"text":"#sn-84","audio":"#t=983.08,996.08"},{"text":"#sn-85","audio":"#t=996.08,1010.96"},{"text":"#sn-86","audio":"#t=1010.96,1032.40"},{"text":"#sn-87","audio":"#t=1032.40,1039.13"},{"text":"#sn-88","audio":"#t=1039.13,1042.45"},{"text":"#sn-89","audio":"#t=1042.45,1047.28"},{"text":"#sn-90","audio":"#t=1047.28,1056.67"},{"text":"#sn-91","audio":"#t=1056.67,1064.16"},{"text":"#sn-92","audio":"#t=1064.16,1078.10"},{"text":"#sn-93","audio":"#t=1078.10,1092.90"},{"text":"#sn-94","audio":"#t=1092.90,1107.22"},{"text":"#sn-95","audio":"#t=1107.22,1116.32"},{"text":"#sn-96","audio":"#t=1116.32,1124.00"},{"text":"#sn-97","audio":"#t=1124.00,1129.79"},{"text":"#sn-98","audio":"#t=1129.79,1136.33"},{"text":"#sn-99","audio":"#t=1136.33,1148.09"},{"text":"#sn-100","audio":"#t=1148.09,1162.22"}]} -------------------------------------------------------------------------------- /content/The-Blue-Fairy-Book-with-sync-narr-alternate/sync/THE-MASTER-CAT;-OR,-PUSS-IN-BOOTS.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "text": "../html/THE-MASTER-CAT;-OR,-PUSS-IN-BOOTS.html", 4 | "audio": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_14_lang_64kb.mp3", 5 | "sync-media-css-class-active": "-active-element", 6 | "sync-media-css-class-playing": "-document-playing" 7 | }, 8 | "role": "document", 9 | "narration": [ 10 | { 11 | "text": "#sn-0", 12 | "audio": "#t=18.97,22.32" 13 | }, 14 | { 15 | "text": "#sn-1", 16 | "audio": "#t=22.32,45.07" 17 | }, 18 | { 19 | "text": "#sn-2", 20 | "audio": "#t=45.07,56.74" 21 | }, 22 | { 23 | "text": "#sn-3", 24 | "audio": "#t=56.74,63.05" 25 | }, 26 | { 27 | "text": "#sn-4", 28 | "audio": "#t=63.05,76.09" 29 | }, 30 | { 31 | "text": "#sn-5", 32 | "audio": "#t=76.09,119.51" 33 | }, 34 | { 35 | "text": "#sn-6", 36 | "audio": "#t=119.51,141.96" 37 | }, 38 | { 39 | "text": "#sn-7", 40 | "audio": "#t=141.96,153.10" 41 | }, 42 | { 43 | "text": "#sn-8", 44 | "audio": "#t=153.10,158.30" 45 | }, 46 | { 47 | "text": "#sn-9", 48 | "audio": "#t=158.30,180.26" 49 | }, 50 | { 51 | "text": "#sn-10", 52 | "audio": "#t=180.26,195.94" 53 | }, 54 | { 55 | "text": "#sn-11", 56 | "audio": "#t=195.94,205.15" 57 | }, 58 | { 59 | "text": "#sn-12", 60 | "audio": "#t=205.15,214.39" 61 | }, 62 | { 63 | "text": "#sn-13", 64 | "audio": "#t=214.39,218.44" 65 | }, 66 | { 67 | "text": "#sn-14", 68 | "audio": "#t=218.44,245.02" 69 | }, 70 | { 71 | "text": "#sn-15", 72 | "audio": "#t=245.02,255.33" 73 | }, 74 | { 75 | "text": "#sn-16", 76 | "audio": "#t=255.33,287.19" 77 | }, 78 | { 79 | "text": "#sn-17", 80 | "audio": "#t=287.19,296.11" 81 | }, 82 | { 83 | "text": "#sn-18", 84 | "audio": "#t=296.11,301.37" 85 | }, 86 | { 87 | "text": "#sn-19", 88 | "audio": "#t=301.37,307.98" 89 | }, 90 | { 91 | "text": "#sn-20", 92 | "audio": "#t=307.98,313.86" 93 | }, 94 | { 95 | "text": "#sn-21", 96 | "audio": "#t=313.86,318.91" 97 | }, 98 | { 99 | "text": "#sn-22", 100 | "audio": "#t=318.91,327.65" 101 | }, 102 | { 103 | "text": "#sn-23", 104 | "audio": "#t=327.65,334.32" 105 | }, 106 | { 107 | "text": "#sn-24", 108 | "audio": "#t=334.32,352.55" 109 | }, 110 | { 111 | "text": "#sn-25", 112 | "audio": "#t=352.55,375.10" 113 | }, 114 | { 115 | "text": "#sn-26", 116 | "audio": "#t=375.10,379.80" 117 | }, 118 | { 119 | "text": "#sn-27", 120 | "audio": "#t=379.80,390.86" 121 | }, 122 | { 123 | "text": "#sn-28", 124 | "audio": "#t=390.86,397.14" 125 | }, 126 | { 127 | "text": "#sn-29", 128 | "audio": "#t=397.14,418.29" 129 | }, 130 | { 131 | "text": "#sn-30", 132 | "audio": "#t=418.29,432.96" 133 | }, 134 | { 135 | "text": "#sn-31", 136 | "audio": "#t=432.96,437.13" 137 | }, 138 | { 139 | "text": "#sn-32", 140 | "audio": "#t=437.13,445.42" 141 | }, 142 | { 143 | "text": "#sn-33", 144 | "audio": "#t=445.42,457.35" 145 | }, 146 | { 147 | "text": "#sn-34", 148 | "audio": "#t=457.35,461.31" 149 | }, 150 | { 151 | "text": "#sn-35", 152 | "audio": "#t=461.31,473.06" 153 | }, 154 | { 155 | "text": "#sn-36", 156 | "audio": "#t=473.06,502.70" 157 | }, 158 | { 159 | "text": "#sn-37", 160 | "audio": "#t=502.70,507.33" 161 | }, 162 | { 163 | "text": "#sn-38", 164 | "audio": "#t=507.33,515.50" 165 | }, 166 | { 167 | "text": "#sn-39", 168 | "audio": "#t=515.50,521.11" 169 | } 170 | ] 171 | } -------------------------------------------------------------------------------- /web/player/audio.js: -------------------------------------------------------------------------------- 1 | import * as Events from './events.js'; 2 | 3 | /* Audio events: 4 | Play 5 | Pause 6 | PositionChange 7 | ClipDone 8 | */ 9 | 10 | let settings = { 11 | volume: 0.8, 12 | rate: 1.0 13 | }; 14 | let clip = { 15 | start: 0, 16 | end: 0, 17 | file: '', 18 | isLastClip: false, 19 | autoplay: true 20 | }; 21 | let audio = null; 22 | let waitForSeek = false; 23 | 24 | function loadFile(file) { 25 | log.debug("Audio Player: file = ", file); 26 | clip.file = file; 27 | let wasMuted = false; 28 | if (audio) { 29 | audio.pause(); 30 | wasMuted = audio.muted; 31 | } 32 | audio = new Audio(file); 33 | audio.currentTime = 0; 34 | audio.muted = wasMuted; 35 | audio.volume = settings.volume; 36 | audio.playbackRate = settings.rate; 37 | audio.addEventListener('progress', e => { onAudioProgress(e) }); 38 | audio.addEventListener('timeupdate', e => { onAudioTimeUpdate(e) }); 39 | } 40 | 41 | function playClip(file, autoplay, start = 0, end = -1, isLastClip = false) { 42 | clip.start = parseFloat(start); 43 | clip.end = parseFloat(end); 44 | clip.isLastClip = isLastClip; 45 | clip.autoplay = autoplay; 46 | if (file != clip.file) { 47 | loadFile(file); 48 | } 49 | else { 50 | waitForSeek = true; 51 | // check that the current time is far enough from the desired start time 52 | // otherwise it stutters due to the coarse granularity of the browser's timeupdate event 53 | if (audio.currentTime < clip.start - .10 || audio.currentTime > clip.start + .10) { 54 | audio.currentTime = clip.start; 55 | } 56 | else { 57 | // log.debug("Audio Player: close enough, not resetting"); 58 | } 59 | } 60 | } 61 | 62 | async function pause() { 63 | if (audio) { 64 | Events.trigger('Audio.Pause'); 65 | await audio.pause(); 66 | } 67 | } 68 | 69 | async function resume() { 70 | Events.trigger('Audio.Play'); 71 | await audio.play(); 72 | } 73 | 74 | function isPlaying() { 75 | return !!(audio.currentTime > 0 76 | && !audio.paused 77 | && !audio.ended 78 | && audio.readyState > 2); 79 | } 80 | 81 | 82 | // this event fires when the file downloads/is downloading 83 | async function onAudioProgress(event) { 84 | // if the file is playing while the rest of it is downloading, 85 | // this function will get called a few times 86 | // we don't want it to reset playback so check that current time is zero before proceeding 87 | if (audio.currentTime == 0 && !isPlaying()) { 88 | log.debug("Audio Player: starting playback"); 89 | audio.currentTime = clip.start; 90 | 91 | if (clip.autoplay) { 92 | Events.trigger('Audio.Play'); 93 | await audio.play(); 94 | } 95 | else { 96 | Events.trigger('Audio.Pause'); 97 | } 98 | } 99 | } 100 | 101 | // this event fires when the playback position changes 102 | async function onAudioTimeUpdate(event) { 103 | Events.trigger('Audio.PositionChange', audio.currentTime, audio.duration); 104 | 105 | if (waitForSeek) { 106 | waitForSeek = false; 107 | Events.trigger('Audio.Play'); 108 | await audio.play(); 109 | } 110 | else { 111 | if (clip.end != -1 && audio.currentTime >= clip.end) { 112 | if (clip.isLastClip) { 113 | Events.trigger('Audio.Pause'); 114 | audio.pause(); 115 | } 116 | Events.trigger("Audio.ClipDone", clip.file); 117 | } 118 | else if (audio.currentTime >= audio.duration && audio.ended) { 119 | Events.trigger('Audio.Pause'); 120 | audio.pause(); 121 | log.debug("Audio Player: element ended playback"); 122 | Events.trigger("Audio.ClipDone", clip.file); 123 | } 124 | } 125 | } 126 | 127 | function setRate(val) { 128 | settings.rate = val; 129 | if (audio) { 130 | audio.playbackRate = val; 131 | } 132 | } 133 | 134 | function setPosition(val) { 135 | if (audio) { 136 | if (val < 0){ 137 | audio.currentTime = 0; 138 | } 139 | else if (val > audio.duration) { 140 | audio.currentTime = audio.duration; 141 | } 142 | else { 143 | audio.currentTime = val; 144 | } 145 | } 146 | } 147 | 148 | function setVolume(val) { 149 | settings.volume = val; 150 | if (audio) { 151 | audio.volume = val; 152 | } 153 | } 154 | 155 | function getPosition() { 156 | if (audio) { 157 | return audio.currentTime; 158 | } 159 | else { 160 | return 0; 161 | } 162 | } 163 | 164 | function mute() { 165 | if (audio) { 166 | audio.muted = true; 167 | } 168 | } 169 | 170 | function unmute() { 171 | if (audio) { 172 | audio.muted = false; 173 | } 174 | } 175 | function isMuted() { 176 | if (audio) { 177 | return audio.muted; 178 | } 179 | return false; 180 | } 181 | export { 182 | playClip, 183 | isPlaying, 184 | pause, 185 | resume, 186 | setRate, 187 | setPosition, 188 | getPosition, 189 | setVolume, 190 | mute, 191 | unmute, 192 | isMuted 193 | }; 194 | -------------------------------------------------------------------------------- /content/The-Blue-Fairy-Book-with-sync-narr-alternate/html/TOADS-AND-DIAMONDS.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TOADS AND DIAMONDS 6 | 7 | 8 | 9 |

TOADS AND DIAMONDS

10 |

THERE was once upon a time a widow who had two daughters. The eldest was so much like her in the face and humor 11 | that whoever looked upon the daughter saw the mother. They were both so disagreeable and so proud that there was 12 | no living with them.

13 |

The youngest, who was the very picture of her father for courtesy and sweetness of temper, was withal one of the 14 | most beautiful girls ever seen. As people naturally love their own likeness, this mother even doted on her 15 | eldest daughter and at the same time had a horrible aversion for the youngest—she made her eat in the kitchen 16 | and work continually.

17 |

Among other things, this poor child was forced twice a day to draw water above a mile and a-half off the house, 18 | and bring home a pitcher full of it. One day, as she was at this fountain, there came to her a poor woman, who 19 | begged of her to let her drink.

20 |

“Oh! ay, with all my heart, Goody,” said this pretty little girl; and rinsing immediately the pitcher, she took 21 | up some water from the clearest place of the fountain, and gave it to her, holding up the pitcher all the while, 22 | that she might drink the easier.

23 |

The good woman, having drunk, said to her:

24 |

“You are so very pretty, my dear, so good and so mannerly, that I cannot help giving you a gift.” For this was a 25 | fairy, who had taken the form of a poor country woman, to see how far the civility and good manners of this 26 | pretty girl would go. “I will give you for a gift,” continued the Fairy, “that, at every word you speak, there 27 | shall come out of your mouth either a flower or a jewel.”

28 |

When this pretty girl came home her mother scolded her for staying so long at the fountain.

29 |

“I beg your pardon, mamma,” said the poor girl, “for not making more haste.”

30 |

And in speaking these words there came out of her mouth two roses, two pearls, and two diamonds.

31 |

“What is it I see there?” said the mother, quite astonished. “I think I see pearls and diamonds come out of the 32 | girl’s mouth! How happens this, child?”

33 |

This was the first time she had ever called her child.

34 |

The poor creature told her frankly all the matter, not without dropping out infinite numbers of diamonds.

35 |

“In good faith,” cried the mother, “I must send my child thither. Come hither, Fanny; look what comes out of thy 36 | sister’s mouth when she speaks. Wouldst not thou be glad, my dear, to have the same gift given thee? Thou hast 37 | nothing else to do but go and draw water out of the fountain, and when a certain poor woman asks you to let her 38 | drink, to give it to her very civilly.”

39 |

“It would be a very fine sight indeed,” said this ill-bred minx, “to see me go draw water.”

40 |

“You shall go, hussy!” said the mother; “and this minute.”

41 |

So away she went, but grumbling all the way, taking with her the best silver tankard in the house.

42 |

She was no sooner at the fountain than she saw coming out of the wood a lady most gloriously dressed, who came up 43 | to her, and asked to drink. This was, you must know, the very fairy who appeared to her sister, but now had 44 | taken the air and dress of a princess, to see how far this girl’s rudeness would go.

45 |

“Am I come hither,” said the proud, saucy one, “to serve you with water, pray? I suppose the silver tankard was 46 | brought purely for your ladyship, was it? However, you may drink out of it, if you have a fancy.”

47 |

“You are not over and above mannerly,” answered the Fairy, without putting herself in a passion. “Well, then, 48 | since you have so little breeding, and are so disobliging, I give you for a gift that at every word you speak 49 | there shall come out of your mouth a snake or a toad.”

50 |

So soon as her mother saw her coming she cried out:

51 |

“Well, daughter?”

52 |

“Well, mother?” answered the pert hussy, throwing out of her mouth two vipers and two toads.

53 |

“Oh! mercy,” cried the mother; “what is it I see? Oh! it is that wretch her sister who has occasioned all this; 54 | but she shall pay for it”; and immediately she ran to beat her. The poor child fled away from her, and went to 55 | hide herself in the forest, not far from thence.

56 |

The King’s son, then on his return from hunting, met her, and seeing her so very pretty, asked her what she did 57 | there alone and why she cried.

58 |

“Alas! sir, my mamma has turned me out of doors.”

59 |

The King’s son, who saw five or six pearls and as many diamonds come out of her mouth, desired her to tell him 60 | how that happened. She thereupon told him the whole story; and so the King’s son fell in love with her, and, 61 | considering himself that such a gift was worth more than any marriage portion, conducted her to the palace of 62 | the King his father, and there married her.

63 |

As for the sister, she made herself so much hated that her own mother turned her off; and the miserable wretch, 64 | having wandered about a good while without finding anybody to take her in, went to a corner of the wood, and 65 | there died.(1)

66 |

(1) Charles Perrault.

67 | 68 | 69 | -------------------------------------------------------------------------------- /web/player/css/controls.css: -------------------------------------------------------------------------------- 1 | @import "../../common/colors.css"; 2 | 3 | :root { 4 | --enabled-opacity: 80%; 5 | --disabled-opacity: 30%; 6 | } 7 | 8 | footer { 9 | display: grid; 10 | grid-column-gap: 1rem; 11 | grid-template-columns: 20% auto 20%; 12 | grid-template-rows: 1fr; 13 | grid-template-areas: 14 | "adjust transport status"; 15 | } 16 | #adjust { 17 | grid-area: adjust; 18 | display: grid; 19 | } 20 | #transport { 21 | grid-area: transport; 22 | justify-self: center; 23 | display: flex; 24 | align-self: center; 25 | align-items: center; 26 | } 27 | #chapter-progress { 28 | grid-area: status; 29 | align-self: center; 30 | justify-self: right; 31 | } 32 | footer div.slider { 33 | display: grid; 34 | grid-template-columns: 20% 50% 15% 10%; 35 | grid-column-gap: 0.5rem; 36 | align-items: center; 37 | /*! position: relative; */ 38 | /*! left: -48px; */ 39 | } 40 | footer input { 41 | background: var(--workaroundbk); 42 | } 43 | footer input::-moz-range-track{ 44 | background-color: lightgray; 45 | opacity: var(--enabled-opacity); 46 | height: 5px; 47 | border-radius: 5px; 48 | } 49 | footer input::-webkit-slider-runnable-track { 50 | background-color: lightgray; 51 | opacity: var(--enabled-opacity); 52 | height: 5px; 53 | border-radius: 5px; 54 | } 55 | footer input::-ms-track { 56 | background-color: lightgray; 57 | opacity: var(--enabled-opacity); 58 | height: 5px; 59 | border-radius: 5px; 60 | } 61 | footer input::-moz-range-thumb { 62 | height: 20px; 63 | cursor: pointer; 64 | border: none; 65 | } 66 | footer input::-webkit-slider-thumb { 67 | height: 20px; 68 | cursor: pointer; 69 | margin: -4px; /* alignment fix for chrome */ 70 | } 71 | footer input::-ms-thumb { 72 | height: 20px; 73 | cursor: pointer; 74 | } 75 | div.slider.disabled label, div.slider.disabled span { 76 | opacity: var(--disabled-opacity); 77 | } 78 | div.slider.disabled input::-moz-range-track { 79 | opacity: var(--disabled-opacity); 80 | } 81 | div.slider.disabled input::-webkit-slider-runnable-track { 82 | opacity: var(--disabled-opacity); 83 | } 84 | div.slider.disabled input::-ms-track { 85 | opacity: var(--disabled-opacity); 86 | } 87 | div.slider.disabled input::-moz-range-thumb { 88 | opacity: var(--disabled-opacity); 89 | cursor: default; 90 | } 91 | div.slider.disabled input::-webkit-slider-thumb { 92 | opacity: var(--disabled-opacity); 93 | cursor: default; 94 | } 95 | div.slider.disabled input::-ms-thumb { 96 | opacity: var(--disabled-opacity); 97 | cursor: default; 98 | } 99 | 100 | footer button { 101 | width: 4rem; 102 | height: 4rem; 103 | } 104 | 105 | #adjust button { 106 | text-align: left; 107 | height: min-content; 108 | width: 2rem; 109 | height: 2rem; 110 | } 111 | #adjust button svg { 112 | padding: 4px; 113 | } 114 | path { 115 | stroke: white; 116 | stroke-linejoin: round; 117 | stroke-linecap: round; 118 | } 119 | svg { 120 | opacity: 90%; 121 | pointer-events: all; 122 | } 123 | #transport svg:hover { 124 | fill: var(--hover); 125 | stroke: var(--hover); 126 | opacity: 80% !important; 127 | } 128 | #bookmark svg:hover line { 129 | stroke: var(--bk); 130 | } 131 | #mute, #reset-rate { 132 | width: min-content; 133 | padding: 0; 134 | } 135 | line.mute-x:not(.muted) { 136 | display: none; 137 | } 138 | #play-pause { 139 | width: 6rem; 140 | height: 6rem; 141 | } 142 | #chapter-progress span { 143 | display: block; 144 | } 145 | @media (max-width: 768px) { 146 | footer { 147 | display: grid; 148 | grid-template-columns: 1fr; 149 | grid-template-rows: auto; 150 | grid-row-gap: 0.5rem; 151 | grid-template-areas: 152 | "transport" 153 | "adjust" 154 | "status"; 155 | justify-items: center; 156 | } 157 | #transport button:not(#play-pause) { 158 | width: 3rem; 159 | height: 3rem; 160 | } 161 | #play-pause { 162 | width: 4rem; 163 | height: 4rem; 164 | } 165 | #adjust label { 166 | visibility: hidden; 167 | } 168 | #chapter-progress { 169 | margin: 0; 170 | font-size: smaller; 171 | justify-self: center; 172 | } 173 | #chapter-progress .label { 174 | display: none; 175 | } 176 | #volume-wrapper { 177 | display: none; 178 | } 179 | #rate-wrapper { 180 | grid-template-rows: 50% auto auto auto; 181 | } 182 | } 183 | 184 | @media (orientation: landscape) and (max-height: 500px) { 185 | #transport button:not(#play-pause) { 186 | width: 3rem; 187 | height: 3rem; 188 | } 189 | #play-pause { 190 | width: 4rem; 191 | height: 4rem; 192 | } 193 | #adjust { 194 | position: relative; 195 | left: -3rem; 196 | } 197 | #rate-wrapper { 198 | display: grid; 199 | grid-template-areas: 200 | "label slider value" 201 | "reset reset reset"; 202 | grid-column-gap: 0.5rem; 203 | grid-row-gap: 0; 204 | align-items: center; 205 | } 206 | #rate-wrapper label { 207 | grid-area: label; 208 | } 209 | #rate-wrapper input { 210 | grid-area: slider; 211 | } 212 | #rate-wrapper span { 213 | grid-area: value; 214 | } 215 | #rate-wrapper button { 216 | grid-area: reset; 217 | justify-self: center; 218 | } 219 | #adjust label { 220 | visibility: hidden; 221 | } 222 | #volume-wrapper { 223 | display: none; 224 | } 225 | } -------------------------------------------------------------------------------- /content/audiobook/toc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Table of Contents 4 | 5 | 6 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /content/abridged-blue-fairy-books/blue-fairy-syncnarr/sync/HANSEL-AND-GRETTEL.json: -------------------------------------------------------------------------------- 1 | {"properties":{"text":"../html/HANSEL-AND-GRETTEL.html","audio":"http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_24_lang_64kb.mp3","sync-media-css-class-active":"-active-element","sync-media-css-class-playing":"-document-playing"},"role":"document","narration":[{"text":"#sn-0","audio":"#t=27.82,30.09"},{"text":"#sn-1","audio":"#t=30.09,41.57"},{"text":"#sn-2","audio":"#t=41.57,50.58"},{"text":"#sn-3","audio":"#t=50.58,67.17"},{"text":"#sn-4","audio":"#t=67.17,87.37"},{"text":"#sn-5","audio":"#t=87.37,99.70"},{"text":"#sn-6","audio":"#t=99.70,108.80"},{"text":"#sn-7","audio":"#t=108.80,117.53"},{"text":"#sn-8","audio":"#t=117.53,124.92"},{"text":"#sn-9","audio":"#t=124.92,131.28"},{"text":"#sn-10","audio":"#t=131.28,138.86"},{"text":"#sn-11","audio":"#t=138.86,147.49"},{"text":"#sn-12","audio":"#t=147.49,155.27"},{"text":"#sn-13","audio":"#t=155.27,160.67"},{"text":"#sn-14","audio":"#t=160.67,171.39"},{"text":"#sn-15","audio":"#t=171.39,182.01"},{"text":"#sn-16","audio":"#t=182.01,190.93"},{"text":"#sn-17","audio":"#t=190.93,196.43"},{"text":"#sn-18","audio":"#t=196.43,200.12"},{"text":"#sn-19","audio":"#t=200.12,209.32"},{"text":"#sn-20","audio":"#t=209.32,219.56"},{"text":"#sn-21","audio":"#t=219.56,227.81"},{"text":"#sn-22","audio":"#t=227.81,235.69"},{"text":"#sn-23","audio":"#t=235.69,243.84"},{"text":"#sn-24","audio":"#t=243.84,254.84"},{"text":"#sn-25","audio":"#t=254.84,261.00"},{"text":"#sn-26","audio":"#t=261.00,276.65"},{"text":"#sn-27","audio":"#t=276.65,283.38"},{"text":"#sn-28","audio":"#t=283.38,288.51"},{"text":"#sn-29","audio":"#t=288.51,296.38"},{"text":"#sn-30","audio":"#t=296.38,304.15"},{"text":"#sn-31","audio":"#t=304.15,307.85"},{"text":"#sn-32","audio":"#t=307.85,314.49"},{"text":"#sn-33","audio":"#t=314.49,323.40"},{"text":"#sn-34","audio":"#t=323.40,333.17"},{"text":"#sn-35","audio":"#t=333.17,338.86"},{"text":"#sn-36","audio":"#t=338.86,349.77"},{"text":"#sn-37","audio":"#t=349.77,357.64"},{"text":"#sn-38","audio":"#t=357.64,367.59"},{"text":"#sn-39","audio":"#t=367.59,387.60"},{"text":"#sn-40","audio":"#t=387.60,396.52"},{"text":"#sn-41","audio":"#t=396.52,413.68"},{"text":"#sn-42","audio":"#t=413.68,418.33"},{"text":"#sn-43","audio":"#t=418.33,430.75"},{"text":"#sn-44","audio":"#t=430.75,439.38"},{"text":"#sn-45","audio":"#t=439.38,444.41"},{"text":"#sn-46","audio":"#t=444.41,449.81"},{"text":"#sn-47","audio":"#t=449.81,457.68"},{"text":"#sn-48","audio":"#t=457.68,462.80"},{"text":"#sn-49","audio":"#t=462.80,469.82"},{"text":"#sn-50","audio":"#t=469.82,476.74"},{"text":"#sn-51","audio":"#t=476.74,480.54"},{"text":"#sn-52","audio":"#t=480.54,487.93"},{"text":"#sn-53","audio":"#t=487.93,502.92"},{"text":"#sn-54","audio":"#t=502.92,509.93"},{"text":"#sn-55","audio":"#t=509.93,517.61"},{"text":"#sn-56","audio":"#t=517.61,533.17"},{"text":"#sn-57","audio":"#t=533.17,546.25"},{"text":"#sn-58","audio":"#t=546.25,555.55"},{"text":"#sn-59","audio":"#t=555.55,563.99"},{"text":"#sn-60","audio":"#t=563.99,570.62"},{"text":"#sn-61","audio":"#t=570.62,581.44"},{"text":"#sn-62","audio":"#t=581.44,596.51"},{"text":"#sn-63","audio":"#t=596.51,607.04"},{"text":"#sn-64","audio":"#t=607.04,612.54"},{"text":"#sn-65","audio":"#t=612.54,628.38"},{"text":"#sn-66","audio":"#t=628.38,640.32"},{"text":"#sn-67","audio":"#t=640.32,650.09"},{"text":"#sn-68","audio":"#t=650.09,655.02"},{"text":"#sn-69","audio":"#t=655.02,657.58"},{"text":"#sn-70","audio":"#t=657.58,660.14"},{"text":"#sn-71","audio":"#t=660.14,662.33"},{"text":"#sn-72","audio":"#t=662.33,665.08"},{"text":"#sn-73","audio":"#t=665.08,666.88"},{"text":"#sn-74","audio":"#t=666.88,670.29"},{"text":"#sn-75","audio":"#t=670.29,682.24"},{"text":"#sn-76","audio":"#t=682.24,688.88"},{"text":"#sn-77","audio":"#t=688.88,694.38"},{"text":"#sn-78","audio":"#t=694.38,705.66"},{"text":"#sn-79","audio":"#t=705.66,717.80"},{"text":"#sn-80","audio":"#t=717.80,730.60"},{"text":"#sn-81","audio":"#t=730.60,742.36"},{"text":"#sn-82","audio":"#t=742.36,750.90"},{"text":"#sn-83","audio":"#t=750.90,762.66"},{"text":"#sn-84","audio":"#t=762.66,773.66"},{"text":"#sn-85","audio":"#t=773.66,789.21"},{"text":"#sn-86","audio":"#t=789.21,800.97"},{"text":"#sn-87","audio":"#t=800.97,812.06"},{"text":"#sn-88","audio":"#t=812.06,820.41"},{"text":"#sn-89","audio":"#t=820.41,826.86"},{"text":"#sn-90","audio":"#t=826.86,835.58"},{"text":"#sn-91","audio":"#t=835.58,848.67"},{"text":"#sn-92","audio":"#t=848.67,856.73"},{"text":"#sn-93","audio":"#t=856.73,866.97"},{"text":"#sn-94","audio":"#t=866.97,886.60"},{"text":"#sn-95","audio":"#t=886.60,891.62"},{"text":"#sn-96","audio":"#t=891.62,898.74"},{"text":"#sn-97","audio":"#t=898.74,905.94"},{"text":"#sn-98","audio":"#t=905.94,911.73"},{"text":"#sn-99","audio":"#t=911.73,918.65"},{"text":"#sn-100","audio":"#t=918.65,927.19"},{"text":"#sn-101","audio":"#t=927.19,935.53"},{"text":"#sn-102","audio":"#t=935.53,946.53"},{"text":"#sn-103","audio":"#t=946.53,952.88"},{"text":"#sn-104","audio":"#t=952.88,962.84"},{"text":"#sn-105","audio":"#t=962.84,971.66"},{"text":"#sn-106","audio":"#t=971.66,976.12"},{"text":"#sn-107","audio":"#t=976.12,983.32"},{"text":"#sn-108","audio":"#t=983.32,995.75"},{"text":"#sn-109","audio":"#t=995.75,1008.45"},{"text":"#sn-110","audio":"#t=1008.45,1014.43"},{"text":"#sn-111","audio":"#t=1014.43,1018.60"},{"text":"#sn-112","audio":"#t=1018.60,1025.14"},{"text":"#sn-113","audio":"#t=1025.14,1037.66"},{"text":"#sn-114","audio":"#t=1037.66,1041.17"},{"text":"#sn-115","audio":"#t=1041.17,1044.02"},{"text":"#sn-116","audio":"#t=1044.02,1046.58"},{"text":"#sn-117","audio":"#t=1046.58,1049.52"},{"text":"#sn-118","audio":"#t=1049.52,1056.82"},{"text":"#sn-119","audio":"#t=1056.82,1064.21"},{"text":"#sn-120","audio":"#t=1064.21,1078.15"},{"text":"#sn-121","audio":"#t=1078.15,1083.84"},{"text":"#sn-122","audio":"#t=1083.84,1090.67"},{"text":"#sn-123","audio":"#t=1090.67,1100.91"},{"text":"#sn-124","audio":"#t=1100.91,1107.36"},{"text":"#sn-125","audio":"#t=1107.36,1116.84"}]} -------------------------------------------------------------------------------- /utils/add-sync-narr-alternate.js: -------------------------------------------------------------------------------- 1 | // take an audiobook with html alternates, and a directory of sync points, and make a sync narr book 2 | // the sync points must be audacity labels files, named the same as the corresponding HTML file 3 | // e.g. THE-MASTER-CAT;-OR,-PUSS-IN-BOOTS.txt for THE-MASTER-CAT;-OR,-PUSS-IN-BOOTS.html 4 | 5 | const fs = require('fs-extra'); 6 | const path = require('path'); 7 | const program = require('commander'); 8 | const jsdom = require("jsdom"); 9 | const { JSDOM } = jsdom; 10 | const utils = require('./utils'); 11 | 12 | program.version('0.0.1'); 13 | program 14 | .requiredOption('-a, --audiobook ', 'audiobook manifest') 15 | .requiredOption('-s, --sync ', 'sync points folder') 16 | .option('-f, --force', 'Overwrite existing output') 17 | .option('-m, --markup', 'Use narration markup (class="narrate") to determine elements'); 18 | program.parse(process.argv); 19 | 20 | let audiobookPath = path.resolve(__dirname, program.audiobook); 21 | let audioManifest = JSON.parse(fs.readFileSync(audiobookPath)); 22 | let syncPointsPath = path.resolve(__dirname, program.sync); 23 | 24 | let titleDir = `${audioManifest.name.replace(/ /gi, '-')}-with-sync-narr-alternate` 25 | let out = path.resolve(__dirname, 'out'); 26 | let out_title = path.resolve(__dirname, `out/${titleDir}`); 27 | console.log(`Copying ${audiobookPath} to ${out_title}`); 28 | fs.copySync(path.dirname(audiobookPath), out_title); 29 | 30 | let out_sync = path.resolve(__dirname, `out/${titleDir}/sync`); 31 | 32 | if (!fs.existsSync(out)){ 33 | fs.mkdirSync(out); 34 | } 35 | if (!fs.existsSync(out_title)){ 36 | fs.mkdirSync(out_title); 37 | } 38 | if (!fs.existsSync(out_sync)){ 39 | fs.mkdirSync(out_sync); 40 | } 41 | 42 | // generate sync narr files 43 | audioManifest.readingOrder.map(item => { 44 | // open each text file 45 | let textPath = path.resolve(path.dirname(audiobookPath), item.alternate.url); 46 | let textFile = fs.readFileSync(textPath).toString(); 47 | let syncPointsFilepath = path.resolve(syncPointsPath, 48 | path.basename(item.alternate.url).replace('.html', '.txt')); 49 | 50 | // if this reading order entry has a corresponding sync points file: 51 | if (fs.existsSync(syncPointsFilepath)) { 52 | 53 | // parse the HTML file and prepare the elements 54 | const dom = new JSDOM(textFile); 55 | const doc = dom.window.document; 56 | let body = doc.querySelector("body"); 57 | let elms = []; 58 | // if we're choosing elements based on class="narrate" 59 | if (program.markup) { 60 | elms = doc.querySelectorAll("*[class='narrate']"); 61 | elms = Array.from(elms); 62 | } 63 | // else just grab the first body child and proceed through its siblings 64 | else { 65 | let elm = body.firstElementChild; 66 | while (elm != null) { 67 | if (!elm.hasAttribute('id')) { 68 | elms.push(elm); 69 | } 70 | elm = elm.nextElementSibling; 71 | } 72 | } 73 | 74 | let pairs = parseSyncPoints(syncPointsFilepath); 75 | let narration = []; 76 | let count = 0; 77 | let idx = 0; 78 | elms.map(elm => { 79 | elm.setAttribute('id', `sn-${count}`); 80 | count++; 81 | // this approach depends on the number of elements and number of sync points lining up 82 | if (idx < pairs.length ) { 83 | narration.push({ 84 | text: `#${elm.getAttribute('id')}`, 85 | audio: `#t=${pairs[idx].start.toFixed(2)},${pairs[idx].end.toFixed(2)}` 86 | }); 87 | } 88 | else { 89 | console.log("Warning: more elms than pairs ", elm.getAttribute('id')); 90 | } 91 | idx++; 92 | }); 93 | 94 | let syncnarrFilename = path.basename(textPath).replace('.html', '.json'); 95 | let syncnarrPath = path.resolve(out_sync, syncnarrFilename); 96 | let syncnarr = { 97 | properties: { 98 | text: `../${item.alternate.url}`, 99 | audio: item.url, 100 | "sync-media-css-class-active": "-active-element", 101 | "sync-media-css-class-playing": "-document-playing" 102 | }, 103 | role: 'document', 104 | narration 105 | }; 106 | 107 | // save html file 108 | utils.writeOut(path.resolve(out_title, item.alternate.url), dom.serialize(), program.force); 109 | 110 | // save sync narr file 111 | utils.writeOut(syncnarrPath, JSON.stringify(syncnarr), program.force); 112 | 113 | // create association in audiobook manifest - either html or sync narr, depending on 114 | // if the word offset data was available 115 | item.alternate = { 116 | type: "LinkedResource", 117 | url: `sync/${syncnarrFilename}`, 118 | encodingFormat: 'application/vnd.syncnarr+json' 119 | }; 120 | } 121 | }); 122 | 123 | // save audiobook manifest 124 | utils.writeOut( 125 | path.resolve(out_title, path.basename(audiobookPath)), 126 | JSON.stringify(audioManifest), 127 | program.force); 128 | 129 | function parseSyncPoints(filepath) { 130 | let labels = fs.readFileSync(filepath); 131 | let lines = labels.toString().split("\n"); 132 | 133 | let i; 134 | let points = []; 135 | for (i=0; i 2 | 3 | 4 | 5 | RUMPELSTILTZKIN 6 | 7 | 8 | 9 |

RUMPELSTILTZKIN

10 |

There was once upon a time a poor miller who had a very beautiful daughter. Now it happened one day that he had 11 | an audience with the King, and in order to appear a person of some importance he told him that he had a daughter 12 | who could spin straw into gold. “Now that’s a talent worth having,” said the King to the miller; “if your 13 | daughter is as clever as you say, bring her to my palace to-morrow, and I’ll put her to the test.” When the girl 14 | was brought to him he led her into a room full of straw, gave her a spinning-wheel and spindle, and said: “Now 15 | set to work and spin all night till early dawn, and if by that time you haven’t spun the straw into gold you 16 | shall die.” Then he closed the door behind him and left her alone inside.

17 |

So the poor miller’s daughter sat down, and didn’t know what in the world she was to do. She hadn’t the least 18 | idea of how to spin straw into gold, and became at last so miserable that she began to cry. Suddenly the door 19 | opened, and in stepped a tiny little man and said: “Good-evening, Miss Miller-maid; why are you crying so 20 | bitterly?” “Oh!” answered the girl, “I have to spin straw into gold, and haven’t a notion how it’s done.” “What 21 | will you give me if I spin it for you?” asked the manikin. “My necklace,” replied the girl. The little man took 22 | the necklace, sat himself down at the wheel, and whir, whir, whir, the wheel went round three times, and the 23 | bobbin was full. Then he put on another, and whir, whir, whir, the wheel went round three times, and the second 24 | too was full; and so it went on till the morning, when all the straw was spun away, and all the bobbins were 25 | full of gold. As soon as the sun rose the King came, and when he perceived the gold he was astonished and 26 | delighted, but his heart only lusted more than ever after the precious metal. He had the miller’s daughter put 27 | into another room full of straw, much bigger than the first, and bade her, if she valued her life, spin it all 28 | into gold before the following morning. The girl didn’t know what to do, and began to cry; then the door opened 29 | as before, and the tiny little man appeared and said: “What’ll you give me if I spin the straw into gold for 30 | you?” “The ring from my finger,” answered the girl. The manikin took the ring, and whir! round went the 31 | spinning-wheel again, and when morning broke he had spun all the straw into glittering gold. The King was 32 | pleased beyond measure at the sights but his greed for gold was still not satisfied, and he had the miller’s 33 | daughter brought into a yet bigger room full of straw, and said: “You must spin all this away in the night; but 34 | if you succeed this time you shall become my wife.” “She’s only a miller’s daughter, it’s true,” he thought; 35 | “but I couldn’t find a richer wife if I were to search the whole world over.” When the girl was alone the little 36 | man appeared for the third time, and said: “What’ll you give me if I spin the straw for you once again?” “I’ve 37 | nothing more to give,” answered the girl. “Then promise me when you are Queen to give me your first child.” “Who 38 | knows what may not happen before that?” thought the miller’s daughter; and besides, she saw no other way out of 39 | it, so she promised the manikin what he demanded, and he set to work once more and spun the straw into gold. 40 | When the King came in the morning, and found everything as he had desired, he straightway made her his wife, and 41 | the miller’s daughter became a queen.

42 |

When a year had passed a beautiful son was born to her, and she thought no more of the little man, till all of a 43 | sudden one day he stepped into her room and said: “Now give me what you promised.” The Queen was in a great 44 | state, and offered the little man all the riches in her kingdom if he would only leave her the child. But the 45 | manikin said: “No, a living creature is dearer to me than all the treasures in the world.” Then the Queen began 46 | to cry and sob so bitterly that the little man was sorry for her, and said: “I’ll give you three days to guess 47 | my name, and if you find it out in that time you may keep your child.”

48 |

Then the Queen pondered the whole night over all the names she had ever heard, and sent a messenger to scour the 49 | land, and to pick up far and near any names he could come across. When the little man arrived on the following 50 | day she began with Kasper, Melchior, Belshazzar, and all the other names she knew, in a string, but at each one 51 | the manikin called out: “That’s not my name.” The next day she sent to inquire the names of all the people in 52 | the neighborhood, and had a long list of the most uncommon and extraordinary for the little man when he made his 53 | appearance. “Is your name, perhaps, Sheepshanks Cruickshanks, Spindleshanks?” but he always replied: “That’s not 54 | my name.” On the third day the messenger returned and announced: “I have not been able to find any new names, 55 | but as I came upon a high hill round the corner of the wood, where the foxes and hares bid each other 56 | good-night, I saw a little house, and in front of the house burned a fire, and round the fire sprang the most 57 | grotesque little man, hopping on one leg and crying:

58 |
  “To-morrow I brew, to-day I bake,
    And then the child away I’ll 59 | take;
    For little deems my royal dame
    That Rumpelstiltzkin is my 60 | name!”

61 |

You can imagine the Queen’s delight at hearing the name, and when the little man stepped in shortly afterward and 62 | asked: “Now, my lady Queen, what’s my name?” she asked first: “Is your name Conrad?” “No.” “Is your name Harry?” 63 | “No.” “Is your name perhaps, Rumpelstiltzkin?” “Some demon has told you that! some demon has told you that!” 64 | screamed the little man, and in his rage drove his right foot so far into the ground that it sank in up to his 65 | waist; then in a passion he seized the left foot with both hands and tore himself in two.(1)

66 |

(1) Grimm.

67 | 68 | 69 | -------------------------------------------------------------------------------- /content/The-Blue-Fairy-Book-with-sync-narr-alternate/toc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Table of Contents 5 | 6 | 7 | 8 | 9 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /web/player/settings.js: -------------------------------------------------------------------------------- 1 | const HIGHLIGHT = "#ffff00"; 2 | const HIGHLIGHTBK = "#000000"; 3 | const FONTSIZE = "medium"; 4 | const USE_CUSTOM_HIGHLIGHT = false; 5 | 6 | import * as LocalData from '../common/localdata.js'; 7 | document.addEventListener("DOMContentLoaded", async () => { 8 | let urlSearchParams = new URLSearchParams(document.location.search); 9 | if (urlSearchParams.has("from")) { 10 | document 11 | .querySelector("#playerLink") 12 | .setAttribute('href', `../player?q=${urlSearchParams.get('from')}`); 13 | } 14 | 15 | await LocalData.initdb(); 16 | document.querySelector("#clearall").addEventListener("click", async event => { 17 | await LocalData.deleteAll(); 18 | document.querySelector("#status").textContent = "Data cleared"; 19 | document.querySelector("#publications tbody").innerHTML = ''; 20 | }); 21 | 22 | // read fontsize stored value 23 | let currentFontsize = localStorage.getItem("fontsize"); 24 | if (!currentFontsize || currentFontsize == "") { 25 | currentFontsize = FONTSIZE; 26 | localStorage.setItem("fontsize", currentFontsize); 27 | } 28 | 29 | let fontsizeInput = document.querySelector("select[name=fontsize]"); 30 | fontsizeInput.value = currentFontsize; 31 | fontsizeInput.addEventListener("change", e => { 32 | localStorage.setItem("fontsize", e.target.value); 33 | refreshSampleTextStyle(); 34 | }); 35 | document.querySelector("#reset-fontsize").addEventListener("click", e => { 36 | localStorage.setItem("fontsize", FONTSIZE); 37 | fontsizeInput.value = FONTSIZE; 38 | refreshSampleTextStyle(); 39 | }); 40 | 41 | // read "use custom highlight" stored value 42 | let useCustomHighlight = localStorage.getItem("use-custom-highlight") === "true"; 43 | if (!useCustomHighlight || useCustomHighlight == "") { 44 | useCustomHighlight = USE_CUSTOM_HIGHLIGHT; 45 | localStorage.setItem("use-custom-highlight", useCustomHighlight); 46 | } 47 | // listen for changes to 'use custom highlight' 48 | let useCustomHighlightInput = document.querySelector("#use-custom-highlight"); 49 | useCustomHighlightInput.value = useCustomHighlight; 50 | useCustomHighlightInput.addEventListener("change", e => { 51 | localStorage.setItem("use-custom-highlight", e.target.checked); 52 | enableDisableCustomHighlightSection(); 53 | }); 54 | 55 | 56 | // read highlight stored value 57 | let currentHighlightColor = localStorage.getItem("highlight"); 58 | if (!currentHighlightColor || currentHighlightColor == "") { 59 | currentHighlightColor = HIGHLIGHT; 60 | localStorage.setItem("highlight", currentHighlightColor); 61 | } 62 | 63 | // select the right color 64 | document.querySelector("#hl").value = currentHighlightColor; 65 | 66 | // listen for changes to highlight color 67 | document.querySelector("#hl").addEventListener("change", e => { 68 | localStorage.setItem("highlight", e.target.value); 69 | refreshSampleTextStyle(); 70 | }); 71 | 72 | // read highlight-bk stored value 73 | let currentHighlightColorBk = localStorage.getItem("highlight-bk"); 74 | if (!currentHighlightColorBk || currentHighlightColorBk == "") { 75 | currentHighlightColorBk = HIGHLIGHTBK; 76 | localStorage.setItem("highlight-bk", currentHighlightColorBk); 77 | } 78 | 79 | // select the right color 80 | document.querySelector("#hlbk").value = currentHighlightColorBk; 81 | 82 | // listen for changes to highlightbk color 83 | document.querySelector("#hlbk").addEventListener("change", e => { 84 | localStorage.setItem("highlight-bk", e.target.value); 85 | refreshSampleTextStyle(); 86 | }); 87 | 88 | document.querySelector("#reset-highlight").addEventListener("click", e => { 89 | localStorage.setItem("highlight", HIGHLIGHT); 90 | document.querySelector("#hl").value = HIGHLIGHT; 91 | document.querySelector("#hlbk").value = HIGHLIGHTBK; 92 | refreshSampleTextStyle(); 93 | }); 94 | 95 | refreshSampleTextStyle(); 96 | enableDisableCustomHighlightSection(); 97 | await loadBookData(); 98 | 99 | }); 100 | 101 | function refreshSampleTextStyle() { 102 | document.querySelector("#sample-text").style.fontSize = localStorage.getItem("fontsize"); 103 | document.querySelector("#sample-text .highlight").style.color = localStorage.getItem("highlight"); 104 | document.querySelector("#sample-text .highlight").style.backgroundColor = localStorage.getItem("highlight-bk"); 105 | 106 | } 107 | 108 | function enableDisableCustomHighlightSection() { 109 | if (localStorage.getItem("use-custom-highlight") == "true") { 110 | document.querySelector("#use-custom-highlight").checked = true; 111 | document.querySelector("#custom-highlight fieldset").removeAttribute("disabled"); 112 | document.querySelector("#custom-highlight").classList.remove("disabled"); 113 | } 114 | else { 115 | document.querySelector("#use-custom-highlight").checked = false; 116 | document.querySelector("#custom-highlight fieldset").setAttribute("disabled", "disabled"); 117 | document.querySelector("#custom-highlight").classList.add("disabled"); 118 | } 119 | } 120 | 121 | async function clearLastRead(pubid) { 122 | let pubdata = await LocalData.getPositions(pubid); 123 | let position = pubdata.find(item => item.type === "last"); 124 | await LocalData.removePosition(position.id); 125 | } 126 | 127 | async function clearBookmarks(pubid) { 128 | let pubdata = await LocalData.getPositions(pubid); 129 | let positions = pubdata.filter(item => item.type === "bookmark"); 130 | let i; 131 | for (i=0; i { 140 | let tr = document.createElement('tr'); 141 | let tdTitle = document.createElement('td'); 142 | tdTitle.textContent = pub.title; 143 | let tdClearLastRead = document.createElement('td'); 144 | let buttonClearLastRead = document.createElement('button'); 145 | buttonClearLastRead.textContent = "Clear last-read"; 146 | buttonClearLastRead.addEventListener('click', e=>clearLastRead(pub.pubid)); 147 | tdClearLastRead.appendChild(buttonClearLastRead); 148 | 149 | let tdClearBmks = document.createElement('td'); 150 | let buttonClearBmks = document.createElement('button'); 151 | buttonClearBmks.textContent = "Clear bookmarks" 152 | buttonClearBmks.addEventListener('click', e=>clearBookmarks(pub.pubid)); 153 | tdClearBmks.appendChild(buttonClearBmks); 154 | 155 | tr.appendChild(tdTitle); 156 | tr.appendChild(tdClearLastRead); 157 | tr.appendChild(tdClearBmks); 158 | 159 | tbody.appendChild(tr); 160 | }); 161 | } -------------------------------------------------------------------------------- /web/player/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Audiobook Player 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |

Nothing open

19 |
20 | 21 | 27 | 28 |
29 |
30 |
31 |
32 |

33 |

34 |
35 | 41 | 48 |
49 |
50 |
51 | 52 | 53 | 54 | 60 |
61 | 62 |
63 | 64 | 65 | 66 | 69 |
70 |
71 | 72 |
73 | 80 | 86 | 97 | 104 | 116 | 117 |
118 |

119 | Chapter progress: -- 120 |

121 | 122 |
123 | 124 | -------------------------------------------------------------------------------- /web/player/narrator.js: -------------------------------------------------------------------------------- 1 | // processes sync narr json 2 | // controls highlight and audio playback 3 | 4 | import { isInViewport } from '../common/utils.js'; 5 | import * as Events from './events.js'; 6 | import * as Audio from './audio.js'; 7 | 8 | let htmlDocument = null; 9 | let items = []; 10 | let properties = {}; 11 | let documentPlayingClass = '-document-playing'; 12 | let activeElementClass = '-active-element'; 13 | let textids = []; 14 | let position = 0; 15 | let seekToOffsetOneTime = false; 16 | let offsetTimestamp = 0; 17 | let autoplayFirstItem = true; 18 | let previousTextColors = {}; 19 | let startingPosition = 0; 20 | let base = ''; 21 | 22 | /* Narrator events: 23 | Done 24 | Highlight 25 | */ 26 | 27 | function setHtmlDocument(doc) { 28 | htmlDocument = doc; 29 | Events.on("Document.Click", loadFromElement); 30 | } 31 | 32 | function loadJson(json, baseurl, autoplay, offset) { 33 | previousTextColors = {}; 34 | properties = json.properties; 35 | base = baseurl; 36 | autoplayFirstItem = autoplay; 37 | documentPlayingClass = json.properties.hasOwnProperty("sync-media-document-playing") ? 38 | json.properties["sync-media-document-playing"] : documentPlayingClass; 39 | activeElementClass = json.properties.hasOwnProperty("sync-media-active-element") ? 40 | json.properties["sync-media-active-element"] : activeElementClass; 41 | items = flatten(json.narration); 42 | // make sure all text properties are arrays 43 | items = items.map(item => item.hasOwnProperty("text") && !(item.text instanceof Array) ? ({...item, text: [item.text]}) : item); 44 | 45 | Events.off("Audio.ClipDone", onAudioClipDone); 46 | 47 | log.debug("Starting sync narration"); 48 | position = offset != 0 ? findOffsetPosition(offset) : 0; 49 | if (position != 0) { 50 | seekToOffsetOneTime = true; 51 | offsetTimestamp = offset; 52 | } 53 | startingPosition = position; 54 | Events.on("Audio.ClipDone", onAudioClipDone); 55 | render(items[position]); 56 | htmlDocument.getElementsByTagName("body")[0].classList.add(documentPlayingClass); 57 | } 58 | 59 | function next() { 60 | textids = items[position].text.map(textitem => textitem.split("#")[1]); 61 | 62 | resetTextStyle(textids); 63 | 64 | if (position+1 < items.length) { 65 | position++; 66 | log.debug("Loading clip " + position); 67 | render( 68 | items[position], 69 | position+1 >= items.length 70 | ); 71 | } 72 | else { 73 | htmlDocument.getElementsByTagName("body")[0].classList.remove(documentPlayingClass); 74 | log.debug("Narration document done"); 75 | Events.trigger('Narrator.Done', ''); 76 | } 77 | } 78 | 79 | function prev() { 80 | textids = items[position].text.map(textitem => textitem.split("#")[1]); 81 | 82 | resetTextStyle(textids); 83 | 84 | if (position-1 >= 0) { 85 | position--; 86 | log.debug("Loading clip " + position); 87 | render( 88 | items[position], 89 | false 90 | ); 91 | } 92 | else { 93 | htmlDocument.getElementsByTagName("body")[0].classList.remove(documentPlayingClass); 94 | log.debug("Start of narration document"); 95 | } 96 | } 97 | 98 | function render(item, isLast) { 99 | /*if (item['role'] != '') { 100 | // this is a substructure 101 | onCanEscape(item["role"]); 102 | }*/ 103 | textids = item.text.map(textitem => textitem.split("#")[1]); 104 | highlightText(textids); 105 | 106 | let audiofile = item.audio.split("#t=")[0]; 107 | if (audiofile == '') { 108 | audiofile = properties.audio; 109 | } 110 | audiofile = new URL(audiofile, base).href; 111 | let start = item.audio.split("#t=")[1].split(",")[0]; 112 | let end = item.audio.split("#t=")[1].split(",")[1]; 113 | 114 | if (seekToOffsetOneTime) { 115 | start = offsetTimestamp; 116 | seekToOffsetOneTime = false; 117 | } 118 | 119 | let autoplay = startingPosition == position ? autoplayFirstItem : true; 120 | Audio.playClip(audiofile, autoplay, start, end, isLast); 121 | } 122 | 123 | function onAudioClipDone(src) { 124 | // TODO this is not robust 125 | if (src == new URL(properties.audio, base).href) { 126 | resetTextStyle(textids); 127 | next(); 128 | } 129 | // else ignore it, the Audio player generates some extra events 130 | } 131 | 132 | function highlightText(ids) { 133 | let elm; 134 | let text = ''; 135 | ids.map(id => { 136 | elm = htmlDocument.getElementById(id); 137 | text += `${elm.innerHTML}

`; 138 | // save the current colors so we can un-highlight the element later 139 | previousTextColors[id] = {"color": elm.style.color, "bk": elm.style.backgroundColor}; 140 | if (localStorage.getItem("use-custom-highlight") === "true") { 141 | // use custom colors 142 | elm.style.color = localStorage.getItem("highlight"); 143 | elm.style.backgroundColor = localStorage.getItem("highlight-bk"); 144 | } 145 | else { 146 | // use publication defaults 147 | elm.classList.add(activeElementClass); 148 | } 149 | 150 | }); 151 | Events.trigger("Narrator.Highlight", ids, text); 152 | 153 | // this is tricky because we can't possibly scroll all of them into view 154 | // the last element wins, I guess 155 | if (!isInViewport(elm, htmlDocument)) { 156 | elm.scrollIntoView(); 157 | } 158 | } 159 | 160 | function resetTextStyle(ids) { 161 | ids.map(id => { 162 | let elm = htmlDocument.getElementById(id); 163 | elm.classList.remove(activeElementClass); 164 | elm.style.color = previousTextColors[id].color; 165 | elm.style.backgroundColor = previousTextColors[id].bk; 166 | }); 167 | } 168 | 169 | // find the node that includes this offset 170 | // assuming one audio file per syncnarr document 171 | function findOffsetPosition(offset) { 172 | let idx = items.findIndex(item => { 173 | let start = parseFloat(item.audio.split("#t=")[1].split(",")[0]); 174 | let end = parseFloat(item.audio.split("#t=")[1].split(",")[1]); 175 | 176 | return start <= offset && end >= offset; 177 | }); 178 | return idx === -1 ? 0 : idx; 179 | } 180 | 181 | let groupId = 0; 182 | // flatten out any nested items 183 | function flatten (itemsArr, roleValue) { 184 | var flatter = itemsArr.map(item => { 185 | if (item.hasOwnProperty("narration")) { 186 | groupId++; 187 | return flatten(item['narration'], item['role']); 188 | } 189 | else { 190 | item.role = roleValue ? roleValue : ''; 191 | item.groupId = groupId; 192 | return item; 193 | } 194 | }) 195 | .reduce((acc, curr) => acc.concat(curr), []); 196 | groupId--; 197 | return flatter; 198 | } 199 | 200 | function loadFromElement(id) { 201 | textids = items[position].text.map(textitem => textitem.split("#")[1]); 202 | 203 | resetTextStyle(textids); 204 | 205 | // TODO this assumes all IDs are fragment only 206 | let itemIdx = items.findIndex(item => item.text.includes(`#${id}`)); 207 | 208 | position = itemIdx; 209 | 210 | render( 211 | items[position], 212 | position+1 >= items.length 213 | ); 214 | } 215 | 216 | /* 217 | function escape() { 218 | console.log("Escape"); 219 | 220 | let textid = items[position].text.split("#")[1]; 221 | resetTextStyle(textid); 222 | 223 | position = items.slice(position).findIndex(thing => thing.groupId !== items[position].groupId) 224 | + (items.length - items.slice(position).length) - 1; 225 | next(); 226 | } 227 | */ 228 | 229 | export { 230 | loadJson, 231 | setHtmlDocument, 232 | next, 233 | prev 234 | }; -------------------------------------------------------------------------------- /web/player/controls.js: -------------------------------------------------------------------------------- 1 | import * as Events from './events.js'; 2 | import * as Audio from './audio.js'; 3 | import * as Narrator from './narrator.js'; 4 | import * as LocalData from '../common/localdata.js'; 5 | import * as Utils from '../common/utils.js'; 6 | 7 | let isPlaying = false; 8 | let isCaption = false; 9 | let isSyncNarr = false; 10 | 11 | function init() { 12 | document.querySelector("#current-position").textContent = '--'; 13 | 14 | document.querySelector("#rate").addEventListener("input", 15 | e => setPlaybackRate(e.target.value)); 16 | document.querySelector("#volume").addEventListener("input", 17 | e => setPlaybackVolume(e.target.value)); 18 | 19 | document.querySelector("#reset-rate").addEventListener("click", 20 | e => setPlaybackRate(100)); 21 | document.querySelector("#mute").addEventListener("click", e => toggleMute()); 22 | 23 | document.querySelector("#bookmark").addEventListener("click", e => addBookmark()); 24 | 25 | document.querySelector("#next").addEventListener("click", e => next()); 26 | document.querySelector("#prev").addEventListener("click", e => prev()); 27 | 28 | document.querySelector("#caption-page").addEventListener("click", e => { 29 | if (isCaption) { 30 | captionsOff(); 31 | Events.trigger("Captions.Off"); 32 | } 33 | else { 34 | captionsOn(); 35 | Events.trigger("Captions.On"); 36 | } 37 | }); 38 | 39 | document.querySelector("#rate").value = 100; 40 | setPlaybackRate(100); 41 | document.querySelector("#volume").value = 80; 42 | setPlaybackVolume(80); 43 | 44 | Events.on("Audio.PositionChange", onPositionChange); 45 | Events.on("Audio.Play", onPlay); 46 | Events.on("Audio.Pause", onPause); 47 | 48 | document.querySelector("#play-pause").addEventListener("click", e => { 49 | if (isPlaying) { 50 | Audio.pause(); 51 | } 52 | else { 53 | Audio.resume(); 54 | } 55 | }); 56 | } 57 | 58 | function next() { 59 | if (isSyncNarr) { 60 | Narrator.next(); 61 | } 62 | else { 63 | Audio.setPosition(Audio.getPosition() + 30); 64 | } 65 | } 66 | function prev() { 67 | if (isSyncNarr) { 68 | Narrator.prev(); 69 | } 70 | else { 71 | Audio.setPosition(Audio.getPosition() - 30); 72 | } 73 | } 74 | function toggleMute() { 75 | if (Audio.isMuted()) { 76 | document.querySelector("#volume-wrapper").classList.remove("disabled"); 77 | document.querySelector("#volume").disabled = false; 78 | document.querySelector("#mute").setAttribute("title", "Mute"); 79 | document.querySelector("#mute").setAttribute("aria-label", "Mute"); 80 | // make the x disappear on the icon 81 | Array.from(document.querySelectorAll(".mute-x")).map(node => node.classList.remove("muted")); 82 | Audio.unmute(); 83 | } 84 | else { 85 | document.querySelector("#volume-wrapper").classList.add("disabled"); 86 | document.querySelector("#volume").disabled = true; 87 | document.querySelector("#mute").setAttribute("title", "Unmute"); 88 | document.querySelector("#mute").setAttribute("aria-label", "Unmute"); 89 | // make the x appear on the icon 90 | Array.from(document.querySelectorAll(".mute-x")).map(node => node.classList.add("muted")); 91 | Audio.mute(); 92 | } 93 | } 94 | function setPlaybackRate(val) { 95 | document.querySelector("#rate-value").textContent = `${val/100}x`; 96 | if (document.querySelector('#rate').value != val) { 97 | document.querySelector("#rate").value = val; 98 | } 99 | Audio.setRate(val/100); 100 | } 101 | 102 | function setPlaybackVolume(val) { 103 | document.querySelector("#volume-value").textContent = `${val}%`; 104 | Audio.setVolume(val/100); 105 | } 106 | 107 | function onPositionChange(position, fileDuration) { 108 | 109 | let currentPosition = Utils.secondsToHms(position); 110 | let fileLength = '--'; 111 | if (!isNaN(fileDuration)) { 112 | let duration = Utils.secondsToHms(fileDuration); 113 | 114 | // if (document.querySelector("#file-length").textContent != duration) { 115 | // document.querySelector("#file-length").textContent = duration; 116 | // } 117 | fileLength = Utils.secondsToHms(fileDuration); 118 | } 119 | // trim the leading zeros 120 | if (currentPosition.indexOf("00:") == 0) { 121 | currentPosition = currentPosition.slice(3); 122 | } 123 | if (fileLength.indexOf("00:") == 0) { 124 | fileLength = fileLength.slice(3); 125 | } 126 | 127 | document.querySelector("#current-position").innerHTML = `${currentPosition} of ${fileLength}`; 128 | } 129 | 130 | function onPlay() { 131 | document.querySelector("#pause").classList.remove("disabled"); 132 | document.querySelector("#play").classList.add("disabled"); 133 | document.querySelector("#play-pause").setAttribute("aria-label", "Pause"); 134 | document.querySelector("#play-pause").setAttribute("title", "Pause"); 135 | isPlaying = true; 136 | } 137 | 138 | function onPause() { 139 | document.querySelector("#pause").classList.add("disabled"); 140 | document.querySelector("#play").classList.remove("disabled"); 141 | document.querySelector("#play-pause").setAttribute("aria-label", "Play"); 142 | document.querySelector("#play-pause").setAttribute("title", "Play"); 143 | isPlaying = false; 144 | } 145 | 146 | function showSyncNarrationControls() { 147 | log.debug("Controls: show sync narration controls"); 148 | isSyncNarr = true; 149 | document.querySelector("#next").setAttribute("aria-label", "Next phrase"); 150 | document.querySelector("#prev").setAttribute("aria-label", "Previous phrase"); 151 | document.querySelector("#next").setAttribute("title", "Next phrase"); 152 | document.querySelector("#prev").setAttribute("title", "Previous phrase"); 153 | 154 | document.querySelector("#caption-page").classList.remove("disabled"); 155 | if (isCaption) { 156 | document.querySelector("#caption").classList.add("disabled"); 157 | document.querySelector("#page").classList.remove("disabled"); 158 | } 159 | else { 160 | document.querySelector("#caption").classList.remove("disabled"); 161 | document.querySelector("#page").classList.add("disabled"); 162 | } 163 | 164 | } 165 | 166 | function showAudioControls() { 167 | log.debug("Controls: show audio controls"); 168 | isSyncNarr = false; 169 | 170 | document.querySelector("#next").setAttribute("aria-label", "Skip ahead 30 seconds"); 171 | document.querySelector("#prev").setAttribute("aria-label", "Skip back 30 seconds"); 172 | 173 | document.querySelector("#next").setAttribute("title", "Skip ahead 30 seconds"); 174 | document.querySelector("#prev").setAttribute("title", "Skip back 30 seconds"); 175 | 176 | document.querySelector("#caption-page").classList.add("disabled"); 177 | 178 | // turn off the captions if they were on 179 | if (isCaption) { 180 | Events.trigger("Captions.Off"); 181 | } 182 | 183 | } 184 | function addBookmark() { 185 | // request the publication ID from the main player 186 | Events.off("Response.Pubid", _addBookmark); 187 | Events.on("Response.Pubid", _addBookmark); 188 | Events.trigger("Request.Pubid"); 189 | } 190 | async function _addBookmark(id) { 191 | await LocalData.addBookmarkAtCurrentPosition(id); 192 | Events.trigger("Bookmarks.Refresh"); 193 | } 194 | 195 | function captionsOff() { 196 | isCaption = false; 197 | document.querySelector("#caption").classList.remove("disabled"); 198 | document.querySelector("#page").classList.add("disabled"); 199 | document.querySelector("#caption-page").setAttribute("title", "Show captions"); 200 | document.querySelector("#caption-page").setAttribute("aria-label", "Show captions"); 201 | } 202 | 203 | function captionsOn() { 204 | isCaption = true; 205 | document.querySelector("#caption").classList.add("disabled"); 206 | document.querySelector("#page").classList.remove("disabled"); 207 | document.querySelector("#caption-page").setAttribute("title", "Show page"); 208 | document.querySelector("#caption-page").setAttribute("aria-label", "Show page"); 209 | } 210 | export { 211 | init, 212 | showSyncNarrationControls, 213 | showAudioControls 214 | } -------------------------------------------------------------------------------- /content/abridged-blue-fairy-books/blue-fairy-audio-html/html/RUMPELSTILTZKIN.html: -------------------------------------------------------------------------------- 1 | 2 | RUMPELSTILTZKIN 3 | 4 | 5 | 6 | 7 |
8 |

RUMPELSTILTZKIN

9 |

There was once upon a time a poor miller who had a very beautiful daughter. 10 | Now it happened one day that he had 11 | an audience with the King, and in order to appear a person of some importance he told him that he had a daughter 12 | who could spin straw into gold. 13 | 14 | “Now that’s a talent worth having,” said the King to the miller; “if your 15 | daughter is as clever as you say, bring her to my palace to-morrow, and I’ll put her to the test.” 16 | 17 | When the girl 18 | was brought to him he led her into a room full of straw, gave her a spinning-wheel and spindle, and said: “Now 19 | set to work and spin all night till early dawn, and if by that time you haven’t spun the straw into gold you 20 | shall die.” 21 | Then he closed the door behind him and left her alone inside.

22 |

So the poor miller’s daughter sat down, and didn’t know what in the world she was to do. 23 | She hadn’t the least 24 | idea of how to spin straw into gold, and became at last so miserable that she began to cry. 25 | 26 | Suddenly the door 27 | opened, and in stepped a tiny little man and said: “Good-evening, Miss Miller-maid; why are you crying so 28 | bitterly?” 29 | “Oh!” answered the girl, “I have to spin straw into gold, and haven’t a notion how it’s done.” 30 | “What 31 | will you give me if I spin it for you?” asked the manikin. 32 | “My necklace,” replied the girl. 33 | The little man took 34 | the necklace, sat himself down at the wheel, and whir, whir, whir, the wheel went round three times, and the 35 | bobbin was full. 36 | 37 | Then he put on another, and whir, whir, whir, the wheel went round three times, and the second 38 | too was full; and so it went on till the morning, when all the straw was spun away, and all the bobbins were 39 | full of gold. 40 | 41 | As soon as the sun rose the King came, and when he perceived the gold he was astonished and 42 | delighted, but his heart only lusted more than ever after the precious metal. 43 | 44 | He had the miller’s daughter put 45 | into another room full of straw, much bigger than the first, and bade her, if she valued her life, spin it all 46 | into gold before the following morning. 47 | 48 | The girl didn’t know what to do, and began to cry; then the door opened 49 | as before, and the tiny little man appeared and said: “What’ll you give me if I spin the straw into gold for 50 | you?” 51 | “The ring from my finger,” answered the girl. 52 | The manikin took the ring, and whir! round went the 53 | spinning-wheel again, and when morning broke he had spun all the straw into glittering gold. 54 | 55 | The King was 56 | pleased beyond measure at the sights but his greed for gold was still not satisfied, and he had the miller’s 57 | daughter brought into a yet bigger room full of straw, and said: “You must spin all this away in the night; but 58 | if you succeed this time you shall become my wife.” 59 | 60 | “She’s only a miller’s daughter, it’s true,” he thought; 61 | “but I couldn’t find a richer wife if I were to search the whole world over.” 62 | 63 | When the girl was alone the little 64 | man appeared for the third time, and said: “What’ll you give me if I spin the straw for you once again?” 65 | 66 | “I’ve 67 | nothing more to give,” answered the girl. 68 | “Then promise me when you are Queen to give me your first child.” 69 | “Who 70 | knows what may not happen before that?” thought the miller’s daughter; and besides, she saw no other way out of 71 | it, so she promised the manikin what he demanded, and he set to work once more and spun the straw into gold. 72 | 73 | 74 | When the King came in the morning, and found everything as he had desired, he straightway made her his wife, and 75 | the miller’s daughter became a queen. 76 |

77 |

78 | When a year had passed a beautiful son was born to her, and she thought no more of the little man, till all of a 79 | sudden one day he stepped into her room and said: “Now give me what you promised.” 80 | 81 | The Queen was in a great 82 | state, and offered the little man all the riches in her kingdom if he would only leave her the child. 83 | 84 | But the 85 | manikin said: “No, a living creature is dearer to me than all the treasures in the world.” 86 | 87 | Then the Queen began 88 | to cry and sob so bitterly that the little man was sorry for her, and said: “I’ll give you three days to guess 89 | my name, and if you find it out in that time you may keep your child.” 90 |

91 |

92 | Then the Queen pondered the whole night over all the names she had ever heard, and sent a messenger to scour the 93 | land, and to pick up far and near any names he could come across. 94 | 95 | When the little man arrived on the following 96 | day she began with Kasper, Melchior, Belshazzar, and all the other names she knew, in a string, but at each one 97 | the manikin called out: “That’s not my name.” 98 | 99 | The next day she sent to inquire the names of all the people in 100 | the neighborhood, and had a long list of the most uncommon and extraordinary for the little man when he made his 101 | appearance. 102 | 103 | “Is your name, perhaps, Sheepshanks Cruickshanks, Spindleshanks?” but he always replied: “That’s not 104 | my name.” 105 | 106 | On the third day the messenger returned and announced: “I have not been able to find any new names, 107 | but as I came upon a high hill round the corner of the wood, where the foxes and hares bid each other 108 | good-night, I saw a little house, and in front of the house burned a fire, and round the fire sprang the most 109 | grotesque little man, hopping on one leg and crying: 110 |

111 | 112 |
113 | "To-morrow I brew, to-day I bake, 114 |
115 | And then the child away I'll take; 116 |
117 | For little deems my royal dame 118 |
119 | That Rumpelstiltzkin is my name!" 120 |
121 | 122 |

123 | You can imagine the Queen’s delight at hearing the name, and when the little man stepped in shortly afterward and 124 | asked: “Now, my lady Queen, what’s my name?” 125 | 126 | she asked first: “Is your name Conrad?” “No.” “Is your name Harry?” 127 | “No.” 128 | “Is your name perhaps, Rumpelstiltzkin?” 129 | “Some demon has told you that! some demon has told you that!” 130 | screamed the little man, and in his rage drove his right foot so far into the ground that it sank in up to his 131 | waist; then in a passion he seized the left foot with both hands and tore himself in two. 132 |

133 |
134 | -------------------------------------------------------------------------------- /content/abridged-blue-fairy-books/blue-fairy-syncnarr/html/RUMPELSTILTZKIN.html: -------------------------------------------------------------------------------- 1 | 2 | RUMPELSTILTZKIN 3 | 4 | 5 | 6 | 7 |
8 |

RUMPELSTILTZKIN

9 |

There was once upon a time a poor miller who had a very beautiful daughter. 10 | Now it happened one day that he had 11 | an audience with the King, and in order to appear a person of some importance he told him that he had a daughter 12 | who could spin straw into gold. 13 | 14 | “Now that’s a talent worth having,” said the King to the miller; “if your 15 | daughter is as clever as you say, bring her to my palace to-morrow, and I’ll put her to the test.” 16 | 17 | When the girl 18 | was brought to him he led her into a room full of straw, gave her a spinning-wheel and spindle, and said: “Now 19 | set to work and spin all night till early dawn, and if by that time you haven’t spun the straw into gold you 20 | shall die.” 21 | Then he closed the door behind him and left her alone inside.

22 |

So the poor miller’s daughter sat down, and didn’t know what in the world she was to do. 23 | She hadn’t the least 24 | idea of how to spin straw into gold, and became at last so miserable that she began to cry. 25 | 26 | Suddenly the door 27 | opened, and in stepped a tiny little man and said: “Good-evening, Miss Miller-maid; why are you crying so 28 | bitterly?” 29 | “Oh!” answered the girl, “I have to spin straw into gold, and haven’t a notion how it’s done.” 30 | “What 31 | will you give me if I spin it for you?” asked the manikin. 32 | “My necklace,” replied the girl. 33 | The little man took 34 | the necklace, sat himself down at the wheel, and whir, whir, whir, the wheel went round three times, and the 35 | bobbin was full. 36 | 37 | Then he put on another, and whir, whir, whir, the wheel went round three times, and the second 38 | too was full; and so it went on till the morning, when all the straw was spun away, and all the bobbins were 39 | full of gold. 40 | 41 | As soon as the sun rose the King came, and when he perceived the gold he was astonished and 42 | delighted, but his heart only lusted more than ever after the precious metal. 43 | 44 | He had the miller’s daughter put 45 | into another room full of straw, much bigger than the first, and bade her, if she valued her life, spin it all 46 | into gold before the following morning. 47 | 48 | The girl didn’t know what to do, and began to cry; then the door opened 49 | as before, and the tiny little man appeared and said: “What’ll you give me if I spin the straw into gold for 50 | you?” 51 | “The ring from my finger,” answered the girl. 52 | The manikin took the ring, and whir! round went the 53 | spinning-wheel again, and when morning broke he had spun all the straw into glittering gold. 54 | 55 | The King was 56 | pleased beyond measure at the sights but his greed for gold was still not satisfied, and he had the miller’s 57 | daughter brought into a yet bigger room full of straw, and said: “You must spin all this away in the night; but 58 | if you succeed this time you shall become my wife.” 59 | 60 | “She’s only a miller’s daughter, it’s true,” he thought; 61 | “but I couldn’t find a richer wife if I were to search the whole world over.” 62 | 63 | When the girl was alone the little 64 | man appeared for the third time, and said: “What’ll you give me if I spin the straw for you once again?” 65 | 66 | “I’ve 67 | nothing more to give,” answered the girl. 68 | “Then promise me when you are Queen to give me your first child.” 69 | “Who 70 | knows what may not happen before that?” thought the miller’s daughter; and besides, she saw no other way out of 71 | it, so she promised the manikin what he demanded, and he set to work once more and spun the straw into gold. 72 | 73 | 74 | When the King came in the morning, and found everything as he had desired, he straightway made her his wife, and 75 | the miller’s daughter became a queen. 76 |

77 |

78 | When a year had passed a beautiful son was born to her, and she thought no more of the little man, till all of a 79 | sudden one day he stepped into her room and said: “Now give me what you promised.” 80 | 81 | The Queen was in a great 82 | state, and offered the little man all the riches in her kingdom if he would only leave her the child. 83 | 84 | But the 85 | manikin said: “No, a living creature is dearer to me than all the treasures in the world.” 86 | 87 | Then the Queen began 88 | to cry and sob so bitterly that the little man was sorry for her, and said: “I’ll give you three days to guess 89 | my name, and if you find it out in that time you may keep your child.” 90 |

91 |

92 | Then the Queen pondered the whole night over all the names she had ever heard, and sent a messenger to scour the 93 | land, and to pick up far and near any names he could come across. 94 | 95 | When the little man arrived on the following 96 | day she began with Kasper, Melchior, Belshazzar, and all the other names she knew, in a string, but at each one 97 | the manikin called out: “That’s not my name.” 98 | 99 | The next day she sent to inquire the names of all the people in 100 | the neighborhood, and had a long list of the most uncommon and extraordinary for the little man when he made his 101 | appearance. 102 | 103 | “Is your name, perhaps, Sheepshanks Cruickshanks, Spindleshanks?” but he always replied: “That’s not 104 | my name.” 105 | 106 | On the third day the messenger returned and announced: “I have not been able to find any new names, 107 | but as I came upon a high hill round the corner of the wood, where the foxes and hares bid each other 108 | good-night, I saw a little house, and in front of the house burned a fire, and round the fire sprang the most 109 | grotesque little man, hopping on one leg and crying: 110 |

111 | 112 |
113 | "To-morrow I brew, to-day I bake, 114 |
115 | And then the child away I'll take; 116 |
117 | For little deems my royal dame 118 |
119 | That Rumpelstiltzkin is my name!" 120 |
121 | 122 |

123 | You can imagine the Queen’s delight at hearing the name, and when the little man stepped in shortly afterward and 124 | asked: “Now, my lady Queen, what’s my name?” 125 | 126 | she asked first: “Is your name Conrad?” “No.” “Is your name Harry?” 127 | “No.” 128 | “Is your name perhaps, Rumpelstiltzkin?” 129 | “Some demon has told you that! some demon has told you that!” 130 | screamed the little man, and in his rage drove his right foot so far into the ground that it sank in up to his 131 | waist; then in a passion he seized the left foot with both hands and tore himself in two. 132 |

133 |
134 | -------------------------------------------------------------------------------- /utils/add-html-alternate.js: -------------------------------------------------------------------------------- 1 | // process an EPUB 2 file 2 | // unzips it, reads the TOC, makes one HTML5 file per TOC entry, named accordingly 3 | // export a manifest.json readingOrder 4 | // works best when the nav points are a flat list and the HTML is basically a flat list too, contained within 5 | // i.e. this should work well on project gutenberg EPUBs 6 | 7 | const fs = require('fs-extra'); 8 | const path = require('path'); 9 | const program = require('commander'); 10 | const extractZip = require('extract-zip'); 11 | const tmp = require('tmp'); 12 | const xpath = require('xpath'); 13 | const DOMParser = require('xmldom-alpha').DOMParser; 14 | const XMLSerializer = require('xmldom-alpha').XMLSerializer; 15 | const jsdom = require("jsdom"); 16 | const { JSDOM } = jsdom; 17 | 18 | const utils = require('./utils'); 19 | 20 | program.version('0.0.1'); 21 | program 22 | .requiredOption('-e, --epub ', 'EPUB2 file') 23 | .requiredOption('-a, --audiobook ', 'Audiobook manifest') 24 | .option('-f, --force', 'Overwrite existing output'); 25 | program.parse(process.argv); 26 | 27 | tmp.setGracefulCleanup(); 28 | 29 | let audiobookPath = path.resolve(__dirname, program.audiobook); 30 | let audioManifest = JSON.parse(fs.readFileSync(audiobookPath).toString()); 31 | 32 | // configure directories 33 | let titleDir = `${audioManifest.name.replace(/ /gi, '-')}-with-html-alternate` 34 | let out = path.resolve(__dirname, 'out'); 35 | let out_title = path.resolve(__dirname, `out/${titleDir}`); 36 | fs.copySync(path.dirname(audiobookPath), out_title); 37 | 38 | let out_html = path.resolve(__dirname, `out/${titleDir}/html`); 39 | 40 | if (!fs.existsSync(out)){ 41 | fs.mkdirSync(out); 42 | } 43 | if (!fs.existsSync(out_title)){ 44 | fs.mkdirSync(out_title); 45 | } 46 | if (!fs.existsSync(out_html)){ 47 | fs.mkdirSync(out_html); 48 | } 49 | 50 | // start processing 51 | (async () => { 52 | // unzip the EPUB 53 | let tmpdir = await unzip(path.resolve(__dirname, program.epub)); 54 | 55 | const select = xpath.useNamespaces({ 56 | html: 'http://www.w3.org/1999/xhtml', 57 | epub: 'http://www.idpf.org/2007/ops', 58 | ncx: 'http://www.daisy.org/z3986/2005/ncx/', 59 | opf: 'http://www.idpf.org/2007/opf', 60 | dc: 'http://purl.org/dc/elements/1.1/' 61 | }); 62 | 63 | 64 | const packageDocPath = calculatePackageDocPath(tmpdir); 65 | 66 | // merge all the content docs into one giant one 67 | const packagedoc = new DOMParser({ errorHandler }).parseFromString(fs.readFileSync(packageDocPath).toString()); 68 | const spineItemIdrefs = select('//opf:itemref/@idref', packagedoc); 69 | let allBodyChildren = []; 70 | spineItemIdrefs.map(idref => { 71 | const manifestItem = select(`//opf:item[@id='${idref.nodeValue}']`, packagedoc); 72 | const filepath = path.join(path.dirname(packageDocPath), manifestItem[0].getAttribute('href')); 73 | const contentDoc = new DOMParser({ errorHandler }).parseFromString(fs.readFileSync(filepath).toString()); 74 | const bodyElm = select('//html:body', contentDoc); 75 | Array.from(bodyElm[0].childNodes).map(node => { 76 | if ((node.hasOwnProperty('tagName') && node.tagName == 'br') 77 | || 78 | (node.textContent.trim() != '' && node.textContent.trim() != '\n') 79 | ) { 80 | allBodyChildren.push(node); 81 | } 82 | }) 83 | }); 84 | 85 | const dom = new JSDOM(`${allBodyChildren.join('')}`); 86 | const bigContentDoc = dom.window.document; 87 | let allNodes = Array.from(bigContentDoc.querySelectorAll("*")); 88 | allNodes.map(node => { 89 | node.removeAttribute("class"); 90 | node.removeAttribute("xmlns"); 91 | node.removeAttribute('tag'); 92 | node.removeAttribute('xml:space'); 93 | }); 94 | 95 | //utils.writeOut(path.resolve(out, 'all.html'), dom.serialize(), true); 96 | 97 | // look at the EPUB NCX 98 | const tocPath = path.resolve(path.dirname(packageDocPath), select('//opf:item[@id="ncx"]', packagedoc)[0].getAttribute('href')); 99 | 100 | const tocdoc = new DOMParser({ errorHandler }).parseFromString( 101 | fs.readFileSync(tocPath).toString(), 102 | 'application/x-dtbncx+xml'); 103 | audioReadingOrderNames = audioManifest.readingOrder.map( 104 | item => utils.stripPunctuation(item.name).toLowerCase() 105 | ); 106 | 107 | // just consider the nav points that have an equivalent (judging by name) in the audiobook manifest 108 | let navPointElms = select("//ncx:navPoint", tocdoc) 109 | .filter(elm => audioReadingOrderNames.includes(utils.stripPunctuation(elm.textContent).toLowerCase())); 110 | 111 | let notIncluded = select('//ncx:navPoint', tocdoc) 112 | .filter(elm => navPointElms.includes(elm) === false); 113 | 114 | console.log("COULD NOT FIND: \n", 115 | notIncluded.map(ni => utils.stripPunctuation(ni.textContent).toLowerCase()).join(', ')); 116 | 117 | let readingOrder = navPointElms 118 | .map(navPoint => { 119 | let startFrag = select("ncx:content/@src", navPoint)[0].value; 120 | startFrag = startFrag.split("").reverse().join(""); 121 | startFrag = startFrag.substr(0, startFrag.indexOf("#")); 122 | startFrag = startFrag.split("").reverse().join(""); 123 | let name = select("ncx:navLabel/ncx:text", navPoint)[0].textContent; 124 | return { 125 | name: name, 126 | url: `${name.replace(/ /gi, '-')}.html`, 127 | start: startFrag 128 | }; 129 | }); 130 | 131 | //console.log(readingOrder); 132 | 133 | readingOrder.map((item, idx) => { 134 | let nextItem = idx + 1 < readingOrder.length ? readingOrder[idx + 1] : {start: "END"}; 135 | let nodes = []; 136 | let startElm = bigContentDoc.querySelector("#" + item.start); 137 | let nextElm = startElm; 138 | let parentElm = startElm.parentElement; 139 | if (startElm.nodeName == "H2") { 140 | let modHeading = bigContentDoc.createElement('h1'); 141 | modHeading.innerHTML = nextElm.innerHTML; 142 | let subsequentElm = startElm.nextSibling; 143 | parentElm.removeChild(startElm); 144 | if (subsequentElm) { 145 | parentElm.insertBefore(modHeading, subsequentElm); 146 | } 147 | else { 148 | parentElm.appendChild(modHeading); 149 | } 150 | nextElm = modHeading; 151 | } 152 | 153 | 154 | while (nextElm != null && nextElm.getAttribute("id") != nextItem.start) { 155 | if (nextElm.textContent.indexOf("End of the Project Gutenberg EBook") != -1) { 156 | break; 157 | } 158 | nodes.push(nextElm); 159 | nextElm = nextElm.nextSibling; 160 | } 161 | 162 | const itemDom = new JSDOM( 163 | `${item.name}${nodes.map(n => n.outerHTML).join('')}`); 164 | 165 | let outfile = `${item.url}`; 166 | let outpath = path.resolve(out_html, outfile); 167 | 168 | utils.writeOut(outpath, itemDom.serialize(), program.force); 169 | 170 | // add to the audio manifest 171 | let audioManifestItem = audioManifest.readingOrder.find( 172 | audioItem => utils.stripPunctuation(audioItem.name).toLowerCase() 173 | === utils.stripPunctuation(item.name).toLowerCase()); 174 | 175 | audioManifestItem.alternate = { 176 | encodingFormat: "text/html", 177 | url: `html/${item.url}`, 178 | type: 'LinkedResource' 179 | }; 180 | }); 181 | 182 | utils.writeOut(`${out_title}/${path.basename(program.audiobook)}`, JSON.stringify(audioManifest), program.force); 183 | 184 | } 185 | 186 | )(); 187 | 188 | // utility to write files 189 | 190 | // UNZIP 191 | async function unzip(path) { 192 | const tmpdir = tmp.dirSync({ unsafeCleanup: true }).name; 193 | return new Promise((resolve, reject) => { 194 | extractZip(path, { dir: tmpdir }, (err) => { 195 | if (err) { 196 | reject(err); 197 | } else { 198 | resolve(tmpdir); 199 | } 200 | }); 201 | }) 202 | } 203 | 204 | // parser requires this 205 | const errorHandler = { 206 | warning: w => console.log("WARNING: ", w), 207 | error: e => console.log("ERROR: ", e), 208 | fatalError: fe => console.log("FATAL ERROR: ", fe), 209 | } 210 | 211 | // get to the package doc 212 | function calculatePackageDocPath(epubDir) { 213 | const containerFilePath = `${epubDir}/META-INF/container.xml`; 214 | const content = fs.readFileSync(containerFilePath).toString(); 215 | const doc = new DOMParser({ errorHandler }).parseFromString(content); 216 | const select = xpath.useNamespaces({ ocf: 'urn:oasis:names:tc:opendocument:xmlns:container' }); 217 | const rootfiles = select('//ocf:rootfile[@media-type="application/oebps-package+xml"]/@full-path', doc); 218 | // just grab the first one as we're not handling the case of multiple renditions 219 | if (rootfiles.length > 0) { 220 | return (path.join(epubDir, rootfiles[0].nodeValue)); 221 | } 222 | return ''; 223 | } 224 | 225 | -------------------------------------------------------------------------------- /content/audiobook/blue.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": ["https://schema.org", "https://www.w3.org/ns/pub-context"], 3 | "conformsTo": "https://www.w3.org/TR/audiobooks/", 4 | "type": "Audiobook", 5 | "id": "https://librivox.org/the-blue-fairy-book-by-andrew-lang/", 6 | "name": "The Blue Fairy Book", 7 | "author": "Andrew Lang", 8 | "readBy": "Various", 9 | "readingOrder": [{ 10 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_01_lang_64kb.mp3", 11 | "encodingFormat": "audio/mpeg", 12 | "name": "The Bronze Ring", 13 | "duration": "PT1258S" 14 | },{ 15 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_02_lang_64kb.mp3", 16 | "encodingFormat": "audio/mpeg", 17 | "name": "Prince Hyacinth and the Dear Little Princess", 18 | "duration": "PT1121S" 19 | },{ 20 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_03_lang_64kb.mp3", 21 | "encodingFormat": "audio/mpeg", 22 | "name": "East of the Sun and West of the Moon", 23 | "duration": "PT1925S" 24 | },{ 25 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_04_lang_64kb.mp3", 26 | "encodingFormat": "audio/mpeg", 27 | "name": "The Yellow Dwarf", 28 | "duration": "PT2982S" 29 | },{ 30 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_05_lang_64kb.mp3", 31 | "encodingFormat": "audio/mpeg", 32 | "name": "Little Red Riding Hood", 33 | "duration": "PT237S" 34 | },{ 35 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_06_lang_64kb.mp3", 36 | "encodingFormat": "audio/mpeg", 37 | "name": "The Sleeping Beauty in the Wood", 38 | "duration": "PT1284S" 39 | },{ 40 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_07_lang_64kb.mp3", 41 | "encodingFormat": "audio/mpeg", 42 | "name": "Cinderella; or, The Little Glass Slipper", 43 | "duration": "PT1170S" 44 | },{ 45 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_08_lang_64kb.mp3", 46 | "encodingFormat": "audio/mpeg", 47 | "name": "Aladdin and the Wonderful Lamp", 48 | "duration": "PT1993S" 49 | },{ 50 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_09_lang_64kb.mp3", 51 | "encodingFormat": "audio/mpeg", 52 | "name": "The Tale of a Youth who Set Out to Learn What Fear Was", 53 | "duration": "PT1175S" 54 | },{ 55 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_10_lang_64kb.mp3", 56 | "encodingFormat": "audio/mpeg", 57 | "name": "Rumpelstiltzkin", 58 | "duration": "PT475S" 59 | },{ 60 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_11_lang_64kb.mp3", 61 | "encodingFormat": "audio/mpeg", 62 | "name": "Beauty and the Beast", 63 | "duration": "PT3081S" 64 | },{ 65 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_12_lang_64kb.mp3", 66 | "encodingFormat": "audio/mpeg", 67 | "name": "The Master-Maid", 68 | "duration": "PT2470S" 69 | },{ 70 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_13_lang_64kb.mp3", 71 | "encodingFormat": "audio/mpeg", 72 | "name": "Why the Sea is Salt", 73 | "duration": "PT655S" 74 | },{ 75 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_14_lang_64kb.mp3", 76 | "encodingFormat": "audio/mpeg", 77 | "name": "The Master Cat; or, Puss in Boots", 78 | "duration": "PT532S" 79 | },{ 80 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_15_lang_64kb.mp3", 81 | "encodingFormat": "audio/mpeg", 82 | "name": "Felicia and the Pot of Pinks", 83 | "duration": "PT1173S" 84 | },{ 85 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_16_lang_64kb.mp3", 86 | "encodingFormat": "audio/mpeg", 87 | "name": "The White Cat", 88 | "duration": "PT2295S" 89 | },{ 90 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_17_lang_64kb.mp3", 91 | "encodingFormat": "audio/mpeg", 92 | "name": "The Water-Lily. The GoldSpinners", 93 | "duration": "PT929S" 94 | },{ 95 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_18_lang_64kb.mp3", 96 | "encodingFormat": "audio/mpeg", 97 | "name": "The Terrible Head", 98 | "duration": "PT1350S" 99 | },{ 100 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_19_lang_64kb.mp3", 101 | "encodingFormat": "audio/mpeg", 102 | "name": "The Story of Pretty Goldilocks", 103 | "duration": "PT1742S" 104 | },{ 105 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_20_lang_64kb.mp3", 106 | "encodingFormat": "audio/mpeg", 107 | "name": "The History of Whittington", 108 | "duration": "PT1029S" 109 | },{ 110 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_21_lang_64kb.mp3", 111 | "encodingFormat": "audio/mpeg", 112 | "name": "The Wonderful Sheep", 113 | "duration": "PT1747S" 114 | },{ 115 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_22_lang_64kb.mp3", 116 | "encodingFormat": "audio/mpeg", 117 | "name": "Little Thumb", 118 | "duration": "PT1353S" 119 | },{ 120 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_23_lang_64kb.mp3", 121 | "encodingFormat": "audio/mpeg", 122 | "name": "The Forty Thieves", 123 | "duration": "PT1105S" 124 | },{ 125 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_24_lang_64kb.mp3", 126 | "encodingFormat": "audio/mpeg", 127 | "name": "Hansel and Grettel", 128 | "duration": "PT1130S" 129 | },{ 130 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_25_lang_64kb.mp3", 131 | "encodingFormat": "audio/mpeg", 132 | "name": "SnowWhite and RoseRed", 133 | "duration": "PT744S" 134 | },{ 135 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_26_lang_64kb.mp3", 136 | "encodingFormat": "audio/mpeg", 137 | "name": "The Goose-Girl", 138 | "duration": "PT723S" 139 | },{ 140 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_27_lang_64kb.mp3", 141 | "encodingFormat": "audio/mpeg", 142 | "name": "Toads and Diamonds", 143 | "duration": "PT325S" 144 | },{ 145 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_28_lang_64kb.mp3", 146 | "encodingFormat": "audio/mpeg", 147 | "name": "Prince Darling", 148 | "duration": "PT1348S" 149 | },{ 150 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_29_lang_64kb.mp3", 151 | "encodingFormat": "audio/mpeg", 152 | "name": "Blue Beard", 153 | "duration": "PT599S" 154 | },{ 155 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_30_lang_64kb.mp3", 156 | "encodingFormat": "audio/mpeg", 157 | "name": "Trusty John", 158 | "duration": "PT1379S" 159 | },{ 160 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_31_lang_64kb.mp3", 161 | "encodingFormat": "audio/mpeg", 162 | "name": "The Brave Little Tailor", 163 | "duration": "PT1487S" 164 | },{ 165 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_32_lang_64kb.mp3", 166 | "encodingFormat": "audio/mpeg", 167 | "name": "A Voyage to Lilliput", 168 | "duration": "PT2495S" 169 | },{ 170 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_33_lang_64kb.mp3", 171 | "encodingFormat": "audio/mpeg", 172 | "name": "The Princess on the Glass Hill", 173 | "duration": "PT1252S" 174 | },{ 175 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_34_lang_64kb.mp3", 176 | "encodingFormat": "audio/mpeg", 177 | "name": "The Story of Prince Ahmed and the Fairy Paribanou", 178 | "duration": "PT4153S" 179 | },{ 180 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_35_lang_64kb.mp3", 181 | "encodingFormat": "audio/mpeg", 182 | "name": "The History of Jack the GiantKiller", 183 | "duration": "PT585S" 184 | },{ 185 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_36_lang_64kb.mp3", 186 | "encodingFormat": "audio/mpeg", 187 | "name": "The Black Bull of Norroway", 188 | "duration": "PT737S" 189 | },{ 190 | "url": "http://www.archive.org/download/blue_fairy_book_0707_librivox/bluefairybook_37_lang_64kb.mp3", 191 | "encodingFormat": "audio/mpeg", 192 | "name": "The Red Etin", 193 | "duration": "PT809S" 194 | }], 195 | "resources": [{ 196 | "url": "https://ia800705.us.archive.org/35/items/blue_fairy_book_0707_librivox/Blue_Fairy_Book_1004.jpg", 197 | "encodingFormat": "image/jpeg", 198 | "rel": "cover" 199 | },{ 200 | "url": "toc.html", 201 | "encodingFormat": "text/html", 202 | "rel": "contents", 203 | "name": "Table of Contents" 204 | },{ 205 | "url": "index.html", 206 | "encodingFormat": "text/html", 207 | "name": "Primary Entry Page" 208 | }] 209 | } 210 | --------------------------------------------------------------------------------