mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-30 18:12:25 -04:00 
			
		
		
		
	Fix: root user password change #93, Change: link to series filter on ab page #90, Add: basic mobi and azw3 ereader support
This commit is contained in:
		
							parent
							
								
									120c70622a
								
							
						
					
					
						commit
						9715c53332
					
				
							
								
								
									
										405
									
								
								client/assets/mobi.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										405
									
								
								client/assets/mobi.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,405 @@ | ||||
| function ab2str(buf) { | ||||
|   if (buf instanceof ArrayBuffer) { | ||||
|     buf = new Uint8Array(buf); | ||||
|   } | ||||
|   return new TextDecoder("utf-8").decode(buf); | ||||
| } | ||||
| 
 | ||||
| var domParser = new DOMParser(); | ||||
| 
 | ||||
| class Buffer { | ||||
|   constructor(capacity) { | ||||
|     this.capacity = capacity; | ||||
|     this.fragment_list = []; | ||||
|     this.cur_fragment = new Fragment(capacity); | ||||
|     this.fragment_list.push(this.cur_fragment); | ||||
|   } | ||||
|   write(byte) { | ||||
|     var result = this.cur_fragment.write(byte); | ||||
|     if (!result) { | ||||
|       this.cur_fragment = new Fragment(this.capacity); | ||||
|       this.fragment_list.push(this.cur_fragment); | ||||
|       this.cur_fragment.write(byte); | ||||
|     } | ||||
|   } | ||||
|   get(idx) { | ||||
|     var fi = 0; | ||||
|     while (fi < this.fragment_list.length) { | ||||
|       var frag = this.fragment_list[fi]; | ||||
|       if (idx < frag.size) { | ||||
|         return frag.get(idx); | ||||
|       } | ||||
|       idx -= frag.size; | ||||
|       fi += 1; | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
|   size() { | ||||
|     var s = 0; | ||||
|     for (var i = 0; i < this.fragment_list.length; i++) { | ||||
|       s += this.fragment_list[i].size; | ||||
|     } | ||||
|     return s; | ||||
|   } | ||||
|   shrink() { | ||||
|     var total_buffer = new Uint8Array(this.size()); | ||||
|     var offset = 0; | ||||
|     for (var i = 0; i < this.fragment_list.length; i++) { | ||||
|       var frag = this.fragment_list[i]; | ||||
|       if (frag.full()) { | ||||
|         total_buffer.set(frag.buffer, offset); | ||||
|       } else { | ||||
|         total_buffer.set(frag.buffer.slice(0, frag.size), offset); | ||||
|       } | ||||
|       offset += frag.size; | ||||
|     } | ||||
|     return total_buffer; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| var combine_uint8array = function (buffers) { | ||||
|   var total_size = 0; | ||||
|   for (var i = 0; i < buffers.length; i++) { | ||||
|     var buffer = buffers[i]; | ||||
|     total_size += buffer.length; | ||||
|   } | ||||
|   var total_buffer = new Uint8Array(total_size); | ||||
|   var offset = 0; | ||||
|   for (var i = 0; i < buffers.length; i++) { | ||||
|     var buffer = buffers[i]; | ||||
|     total_buffer.set(buffer, offset); | ||||
|     offset += buffer.length; | ||||
|   } | ||||
|   return total_buffer; | ||||
| } | ||||
| 
 | ||||
| class Fragment { | ||||
|   constructor(capacity) { | ||||
|     this.buffer = new Uint8Array(capacity); | ||||
|     this.capacity = capacity; | ||||
|     this.size = 0; | ||||
|   } | ||||
| 
 | ||||
|   write(byte) { | ||||
|     if (this.size >= this.capacity) { | ||||
|       return false; | ||||
|     } | ||||
|     this.buffer[this.size] = byte; | ||||
|     this.size += 1; | ||||
|     return true; | ||||
|   } | ||||
|   full() { | ||||
|     return this.size === this.capacity; | ||||
|   } | ||||
|   get(idx) { | ||||
|     return this.buffer[idx]; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| var uncompression_lz77 = function (data) { | ||||
|   var length = data.length; | ||||
|   var offset = 0;   // Current offset into data
 | ||||
|   var buffer = new Buffer(data.length); | ||||
| 
 | ||||
|   while (offset < length) { | ||||
|     var char = data[offset]; | ||||
|     offset += 1; | ||||
| 
 | ||||
|     if (char == 0) { | ||||
|       buffer.write(char); | ||||
|     } else if (char <= 8) { | ||||
|       for (var i = offset; i < offset + char; i++) { | ||||
|         buffer.write(data[i]); | ||||
|       } | ||||
|       offset += char; | ||||
|     } else if (char <= 0x7f) { | ||||
|       buffer.write(char); | ||||
|     } else if (char <= 0xbf) { | ||||
|       var next = data[offset]; | ||||
|       offset += 1; | ||||
|       var distance = ((char << 8 | next) >> 3) & 0x7ff; | ||||
|       var lz_length = (next & 0x7) + 3; | ||||
| 
 | ||||
|       var buffer_size = buffer.size(); | ||||
|       for (var i = 0; i < lz_length; i++) { | ||||
|         buffer.write(buffer.get(buffer_size - distance)) | ||||
|         buffer_size += 1; | ||||
|       } | ||||
|     } else { | ||||
|       buffer.write(32); | ||||
|       buffer.write(char ^ 0x80); | ||||
|     } | ||||
|   } | ||||
|   return buffer; | ||||
| }; | ||||
| 
 | ||||
| class MobiFile { | ||||
|   constructor(data) { | ||||
|     this.view = new DataView(data); | ||||
|     this.buffer = this.view.buffer; | ||||
|     this.offset = 0; | ||||
|     this.header = null; | ||||
|   } | ||||
| 
 | ||||
|   parse() { | ||||
| 
 | ||||
|   } | ||||
| 
 | ||||
|   getUint8() { | ||||
|     var v = this.view.getUint8(this.offset); | ||||
|     this.offset += 1; | ||||
|     return v; | ||||
|   } | ||||
| 
 | ||||
|   getUint16() { | ||||
|     var v = this.view.getUint16(this.offset); | ||||
|     this.offset += 2; | ||||
|     return v; | ||||
|   } | ||||
| 
 | ||||
|   getUint32() { | ||||
|     var v = this.view.getUint32(this.offset); | ||||
|     this.offset += 4; | ||||
|     return v; | ||||
|   } | ||||
| 
 | ||||
|   getStr(size) { | ||||
|     var v = ab2str(this.buffer.slice(this.offset, this.offset + size)); | ||||
|     this.offset += size; | ||||
|     return v; | ||||
|   } | ||||
| 
 | ||||
|   skip(size) { | ||||
|     this.offset += size; | ||||
|   } | ||||
| 
 | ||||
|   setoffset(_of) { | ||||
|     this.offset = _of; | ||||
|   } | ||||
| 
 | ||||
|   get_record_extrasize(data, flags) { | ||||
|     var pos = data.length - 1; | ||||
|     var extra = 0; | ||||
|     for (var i = 15; i > 0; i--) { | ||||
|       if (flags & (1 << i)) { | ||||
|         var res = this.buffer_get_varlen(data, pos); | ||||
|         var size = res[0]; | ||||
|         var l = res[1]; | ||||
|         pos = res[2]; | ||||
|         pos -= size - l; | ||||
|         extra += size; | ||||
|       } | ||||
|     } | ||||
|     if (flags & 1) { | ||||
|       var a = data[pos]; | ||||
|       extra += (a & 0x3) + 1; | ||||
|     } | ||||
|     return extra; | ||||
|   } | ||||
| 
 | ||||
|   // data should be uint8array
 | ||||
|   buffer_get_varlen(data, pos) { | ||||
|     var l = 0; | ||||
|     var size = 0; | ||||
|     var byte_count = 0; | ||||
|     var mask = 0x7f; | ||||
|     var stop_flag = 0x80; | ||||
|     var shift = 0; | ||||
|     for (var i = 0; ; i++) { | ||||
|       var byte = data[pos]; | ||||
|       size |= (byte & mask) << shift; | ||||
|       shift += 7; | ||||
|       l += 1; | ||||
|       byte_count += 1; | ||||
|       pos -= 1; | ||||
| 
 | ||||
|       var to_stop = byte & stop_flag; | ||||
|       if (byte_count >= 4 || to_stop > 0) { | ||||
|         break; | ||||
|       } | ||||
|     } | ||||
|     return [size, l, pos]; | ||||
|   } | ||||
| 
 | ||||
|   read_text() { | ||||
|     var text_end = this.palm_header.record_count; | ||||
|     var buffers = []; | ||||
|     for (var i = 1; i <= text_end; i++) { | ||||
|       buffers.push(this.read_text_record(i)); | ||||
|     } | ||||
|     var all = combine_uint8array(buffers) | ||||
|     return ab2str(all); | ||||
|   } | ||||
| 
 | ||||
|   read_text_record(i) { | ||||
|     var flags = this.mobi_header.extra_flags; | ||||
|     var begin = this.reclist[i].offset; | ||||
|     var end = this.reclist[i + 1].offset; | ||||
| 
 | ||||
|     var data = new Uint8Array(this.buffer.slice(begin, end)) | ||||
|     var ex = this.get_record_extrasize(data, flags); | ||||
| 
 | ||||
|     data = new Uint8Array(this.buffer.slice(begin, end - ex)); | ||||
|     if (this.palm_header.compression === 2) { | ||||
|       var buffer = uncompression_lz77(data); | ||||
|       return buffer.shrink(); | ||||
|     } else { | ||||
|       return data; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   read_image(idx) { | ||||
|     var first_image_idx = this.mobi_header.first_image_idx; | ||||
|     var begin = this.reclist[first_image_idx + idx].offset; | ||||
|     var end = this.reclist[first_image_idx + idx + 1].offset; | ||||
|     var data = new Uint8Array(this.buffer.slice(begin, end)) | ||||
|     return new Blob([data.buffer]); | ||||
|   } | ||||
| 
 | ||||
|   load() { | ||||
|     this.header = this.load_pdbheader(); | ||||
|     this.reclist = this.load_reclist(); | ||||
|     this.load_record0(); | ||||
|   } | ||||
| 
 | ||||
|   load_pdbheader() { | ||||
|     var header = {}; | ||||
|     header.name = this.getStr(32); | ||||
|     header.attr = this.getUint16(); | ||||
|     header.version = this.getUint16(); | ||||
|     header.ctime = this.getUint32(); | ||||
|     header.mtime = this.getUint32(); | ||||
|     header.btime = this.getUint32(); | ||||
|     header.mod_num = this.getUint32(); | ||||
|     header.appinfo_offset = this.getUint32(); | ||||
|     header.sortinfo_offset = this.getUint32(); | ||||
|     header.type = this.getStr(4); | ||||
|     header.creator = this.getStr(4); | ||||
|     header.uid = this.getUint32(); | ||||
|     header.next_rec = this.getUint32(); | ||||
|     header.record_num = this.getUint16(); | ||||
| 
 | ||||
|     return header; | ||||
|   } | ||||
| 
 | ||||
|   load_reclist() { | ||||
|     var reclist = []; | ||||
|     for (var i = 0; i < this.header.record_num; i++) { | ||||
|       var record = {}; | ||||
|       record.offset = this.getUint32(); | ||||
|       // TODO(zz) change 
 | ||||
|       record.attr = this.getUint32(); | ||||
|       reclist.push(record); | ||||
|     } | ||||
|     return reclist; | ||||
|   } | ||||
|   load_record0() { | ||||
|     this.palm_header = this.load_record0_header(); | ||||
|     this.mobi_header = this.load_mobi_header(); | ||||
|   } | ||||
| 
 | ||||
|   load_record0_header() { | ||||
|     var p_header = {}; | ||||
|     var first_record = this.reclist[0]; | ||||
|     this.setoffset(first_record.offset); | ||||
| 
 | ||||
|     p_header.compression = this.getUint16(); | ||||
|     this.skip(2); | ||||
|     p_header.text_length = this.getUint32(); | ||||
|     p_header.record_count = this.getUint16(); | ||||
|     p_header.record_size = this.getUint16(); | ||||
|     p_header.encryption_type = this.getUint16(); | ||||
|     this.skip(2); | ||||
| 
 | ||||
|     return p_header; | ||||
|   } | ||||
| 
 | ||||
|   load_mobi_header() { | ||||
|     var mobi_header = {}; | ||||
| 
 | ||||
|     var start_offset = this.offset; | ||||
| 
 | ||||
|     mobi_header.identifier = this.getUint32(); | ||||
|     mobi_header.header_length = this.getUint32(); | ||||
|     mobi_header.mobi_type = this.getUint32(); | ||||
|     mobi_header.text_encoding = this.getUint32(); | ||||
|     mobi_header.uid = this.getUint32(); | ||||
|     mobi_header.generator_version = this.getUint32(); | ||||
| 
 | ||||
|     this.skip(40); | ||||
| 
 | ||||
|     mobi_header.first_nonbook_index = this.getUint32(); | ||||
|     mobi_header.full_name_offset = this.getUint32(); | ||||
|     mobi_header.full_name_length = this.getUint32(); | ||||
| 
 | ||||
|     mobi_header.language = this.getUint32(); | ||||
|     mobi_header.input_language = this.getUint32(); | ||||
|     mobi_header.output_language = this.getUint32(); | ||||
|     mobi_header.min_version = this.getUint32(); | ||||
|     mobi_header.first_image_idx = this.getUint32(); | ||||
| 
 | ||||
|     mobi_header.huff_rec_index = this.getUint32(); | ||||
|     mobi_header.huff_rec_count = this.getUint32(); | ||||
|     mobi_header.datp_rec_index = this.getUint32(); | ||||
|     mobi_header.datp_rec_count = this.getUint32(); | ||||
| 
 | ||||
|     mobi_header.exth_flags = this.getUint32(); | ||||
| 
 | ||||
|     this.skip(36); | ||||
| 
 | ||||
|     mobi_header.drm_offset = this.getUint32(); | ||||
|     mobi_header.drm_count = this.getUint32(); | ||||
|     mobi_header.drm_size = this.getUint32(); | ||||
|     mobi_header.drm_flags = this.getUint32(); | ||||
| 
 | ||||
|     this.skip(8); | ||||
| 
 | ||||
|     // TODO (zz) fdst_index
 | ||||
|     this.skip(4); | ||||
| 
 | ||||
|     this.skip(46); | ||||
| 
 | ||||
|     mobi_header.extra_flags = this.getUint16(); | ||||
| 
 | ||||
|     this.setoffset(start_offset + mobi_header.header_length) | ||||
| 
 | ||||
|     return mobi_header; | ||||
|   } | ||||
|   load_exth_header() { | ||||
|     // TODO
 | ||||
|     return {}; | ||||
|   } | ||||
| 
 | ||||
|   render_to(id) { | ||||
|     this.load(); | ||||
|     var content = this.read_text(); | ||||
| 
 | ||||
|     var bookDom = document.getElementById(id); | ||||
|     while (bookDom.firstChild) { | ||||
|       bookDom.removeChild(bookDom.firstChild); | ||||
|     } | ||||
| 
 | ||||
|     var bookDoc = domParser.parseFromString(content, "text/html"); | ||||
|     bookDoc.body.childNodes.forEach(function (x) { | ||||
|       if (x instanceof Element) { | ||||
|         bookDom.appendChild(x); | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     var imgDoms = bookDom.getElementsByTagName("img"); | ||||
|     for (var i = 0; i < imgDoms.length; i++) { | ||||
|       this.render_image(imgDoms, i); | ||||
|     } | ||||
|   } | ||||
|   render_image(imgDoms, i) { | ||||
|     var imgDom = imgDoms[i]; | ||||
|     var idx = +imgDom.getAttribute("recindex"); | ||||
|     var blob = this.read_image(idx - 1); | ||||
|     var imgReader = new FileReader(); | ||||
|     imgReader.onload = function (e) { | ||||
|       imgDom.src = e.target.result; | ||||
|     }; | ||||
|     imgReader.readAsDataURL(blob); | ||||
|   } | ||||
| } | ||||
| module.exports = MobiFile | ||||
| @ -9,11 +9,14 @@ | ||||
|       </select> | ||||
|     </div> --> | ||||
|     <div class="absolute top-4 left-4 font-book"> | ||||
|       <h1 class="text-2xl mb-1">{{ title }}</h1> | ||||
| 
 | ||||
|       <p v-if="author">by {{ author }}</p> | ||||
|       <h1 class="text-2xl mb-1">{{ title || abTitle }}</h1> | ||||
|       <p v-if="author || abAuthor">by {{ author || abAuthor }}</p> | ||||
|     </div> | ||||
|     <div class="h-full flex items-center"> | ||||
|     <div v-if="!epubEbook && mobiEbook" class="absolute top-4 left-0 w-full flex justify-center"> | ||||
|       <p class="text-error font-semibold">Warning: Reading mobi & azw3 files is in the very early stages</p> | ||||
|     </div> | ||||
| 
 | ||||
|     <div v-if="epubEbook" class="h-full flex items-center"> | ||||
|       <div style="width: 100px; max-width: 100px" class="h-full flex items-center overflow-x-hidden"> | ||||
|         <span v-show="hasPrev" class="material-icons text-black text-opacity-30 hover:text-opacity-80 cursor-pointer text-8xl" @mousedown.prevent @click="pageLeft">chevron_left</span> | ||||
|       </div> | ||||
| @ -28,11 +31,17 @@ | ||||
|         <span v-show="hasNext" class="material-icons text-black text-opacity-30 hover:text-opacity-80 cursor-pointer text-8xl" @mousedown.prevent @click="pageRight">chevron_right</span> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div v-else class="h-full flex items-center justify-center"> | ||||
|       <div class="w-full max-w-4xl overflow-y-auto border border-black border-opacity-10 p-4" style="max-height: 80vh"> | ||||
|         <div id="viewer" /> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import ePub from 'epubjs' | ||||
| import mobijs from '@/assets/mobi.js' | ||||
| 
 | ||||
| export default { | ||||
|   data() { | ||||
| @ -65,6 +74,12 @@ export default { | ||||
|         this.$store.commit('setShowEReader', val) | ||||
|       } | ||||
|     }, | ||||
|     abTitle() { | ||||
|       return this.selectedAudiobook.book.title | ||||
|     }, | ||||
|     abAuthor() { | ||||
|       return this.selectedAudiobook.book.author | ||||
|     }, | ||||
|     selectedAudiobook() { | ||||
|       return this.$store.state.selectedAudiobook | ||||
|     }, | ||||
| @ -83,6 +98,16 @@ export default { | ||||
|     epubPath() { | ||||
|       return this.epubEbook ? this.epubEbook.path : null | ||||
|     }, | ||||
|     mobiEbook() { | ||||
|       return this.ebooks.find((eb) => eb.ext === '.mobi' || eb.ext === '.azw3') | ||||
|     }, | ||||
|     mobiPath() { | ||||
|       return this.mobiEbook ? this.mobiEbook.path : null | ||||
|     }, | ||||
|     mobiUrl() { | ||||
|       if (!this.mobiPath) return null | ||||
|       return `/ebook/${this.libraryId}/${this.folderId}/${this.mobiPath}` | ||||
|     }, | ||||
|     url() { | ||||
|       if (!this.epubPath) return null | ||||
|       return `/ebook/${this.libraryId}/${this.folderId}/${this.epubPath}` | ||||
| @ -134,7 +159,24 @@ export default { | ||||
|     init() { | ||||
|       this.registerListeners() | ||||
| 
 | ||||
|       console.log('epub', this.url, this.epubEbook, this.ebooks) | ||||
|       if (this.epubEbook) { | ||||
|         this.initEpub() | ||||
|       } else if (this.mobiEbook) { | ||||
|         this.initMobi() | ||||
|       } | ||||
|     }, | ||||
|     async initMobi() { | ||||
|       var buff = await this.$axios.$get(this.mobiUrl, { | ||||
|         responseType: 'blob' | ||||
|       }) | ||||
|       var reader = new FileReader() | ||||
|       reader.onload = function (event) { | ||||
|         var file_content = event.target.result | ||||
|         new mobijs(file_content).render_to('viewer') | ||||
|       } | ||||
|       reader.readAsArrayBuffer(buff) | ||||
|     }, | ||||
|     initEpub() { | ||||
|       // var book = ePub(this.url, { | ||||
|       //   requestHeaders: { | ||||
|       //     Authorization: `Bearer ${this.userToken}` | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "audiobookshelf-client", | ||||
|   "version": "1.4.5", | ||||
|   "version": "1.4.6", | ||||
|   "description": "Audiobook manager and player", | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|  | ||||
| @ -23,7 +23,7 @@ | ||||
|           <div class="flex items-center py-2"> | ||||
|             <p v-if="isRoot" class="text-error py-2 text-xs">* Root user is the only user that can have an empty password</p> | ||||
|             <div class="flex-grow" /> | ||||
|             <ui-btn v-show="password && newPassword && confirmPassword" type="submit" :loading="changingPassword" color="success">Submit</ui-btn> | ||||
|             <ui-btn v-show="(password && newPassword && confirmPassword) || isRoot" type="submit" :loading="changingPassword" color="success">Submit</ui-btn> | ||||
|           </div> | ||||
|         </form> | ||||
|       </div> | ||||
|  | ||||
| @ -22,8 +22,7 @@ | ||||
|                 by <nuxt-link v-if="author" :to="`/library/${libraryId}/bookshelf?filter=authors.${$encode(author)}`" class="hover:underline">{{ author }}</nuxt-link | ||||
|                 ><span v-else>Unknown</span> | ||||
|               </p> | ||||
| 
 | ||||
|               <h3 v-if="series" class="font-sans text-gray-300 text-lg leading-7 mb-4">{{ seriesText }}</h3> | ||||
|               <nuxt-link v-if="series" :to="`/library/${libraryId}/bookshelf?filter=series.${$encode(series)}`" class="hover:underline font-sans text-gray-300 text-lg leading-7 mb-4"> {{ seriesText }}</nuxt-link> | ||||
| 
 | ||||
|               <!-- <div class="w-min"> | ||||
|                 <ui-tooltip :text="authorTooltipText" direction="bottom"> | ||||
| @ -105,7 +104,7 @@ | ||||
|               {{ isMissing ? 'Missing' : 'Incomplete' }} | ||||
|             </ui-btn> | ||||
| 
 | ||||
|             <ui-btn v-if="showExperimentalFeatures && epubEbook" color="info" :padding-x="4" small class="flex items-center h-9 mr-2" @click="openEbook"> | ||||
|             <ui-btn v-if="showExperimentalFeatures && (epubEbook || mobiEbook)" color="info" :padding-x="4" small class="flex items-center h-9 mr-2" @click="openEbook"> | ||||
|               <span class="material-icons -ml-2 pr-2 text-white">auto_stories</span> | ||||
|               Read | ||||
|             </ui-btn> | ||||
| @ -329,7 +328,10 @@ export default { | ||||
|       return !this.tracks.length && this.ebooks.length && !this.showExperimentalFeatures | ||||
|     }, | ||||
|     epubEbook() { | ||||
|       return this.audiobook.ebooks.find((eb) => eb.ext === '.epub') | ||||
|       return this.ebooks.find((eb) => eb.ext === '.epub') | ||||
|     }, | ||||
|     mobiEbook() { | ||||
|       return this.ebooks.find((eb) => eb.ext === '.mobi' || eb.ext === '.azw3') | ||||
|     }, | ||||
|     userToken() { | ||||
|       return this.$store.getters['user/getToken'] | ||||
| @ -455,6 +457,11 @@ export default { | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.$store.commit('audiobooks/addListener', { id: 'audiobook', audiobookId: this.audiobookId, meth: this.audiobookUpdated }) | ||||
| 
 | ||||
|     // If a library has not yet been loaded, use this audiobooks library id as the current | ||||
|     if (!this.$store.state.audiobooks.loadedLibraryId && this.libraryId) { | ||||
|       this.$store.commit('libraries/setCurrentLibrary', this.libraryId) | ||||
|     } | ||||
|   }, | ||||
|   beforeDestroy() { | ||||
|     this.$store.commit('audiobooks/removeListener', 'audiobook') | ||||
|  | ||||
| @ -113,7 +113,6 @@ export const mutations = { | ||||
|     state.showEditModal = val | ||||
|   }, | ||||
|   showEReader(state, audiobook) { | ||||
|     console.log('Show EReader', audiobook) | ||||
|     state.selectedAudiobook = audiobook | ||||
|     state.showEReader = true | ||||
|   }, | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "audiobookshelf", | ||||
|   "version": "1.4.5", | ||||
|   "version": "1.4.6", | ||||
|   "description": "Self-hosted audiobook server for managing and playing audiobooks", | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|  | ||||
| @ -131,6 +131,10 @@ class Audiobook { | ||||
|     return this.otherFiles.find(file => file.ext === '.epub') | ||||
|   } | ||||
| 
 | ||||
|   get hasMobi() { | ||||
|     return this.otherFiles.find(file => file.ext === '.mobi' || file.ext === '.azw3') | ||||
|   } | ||||
| 
 | ||||
|   get hasMissingIno() { | ||||
|     return !this.ino || this._audioFiles.find(abf => !abf.ino) || this._otherFiles.find(f => !f.ino) || this._tracks.find(t => !t.ino) | ||||
|   } | ||||
| @ -202,7 +206,7 @@ class Audiobook { | ||||
|       hasInvalidParts: this.invalidParts ? this.invalidParts.length : 0, | ||||
|       // numEbooks: this.ebooks.length,
 | ||||
|       ebooks: this.ebooks.map(ebook => ebook.toJSON()), | ||||
|       numEbooks: this.hasEpub ? 1 : 0, // Only supporting epubs in the reader currently
 | ||||
|       numEbooks: (this.hasEpub || this.hasMobi) ? 1 : 0, // Only supporting epubs in the reader currently
 | ||||
|       numTracks: this.tracks.length, | ||||
|       chapters: this.chapters || [], | ||||
|       isMissing: !!this.isMissing, | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| const globals = { | ||||
|   SupportedImageTypes: ['png', 'jpg', 'jpeg', 'webp'], | ||||
|   SupportedAudioTypes: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'mp4'], | ||||
|   SupportedEbookTypes: ['epub', 'pdf', 'mobi'] | ||||
|   SupportedEbookTypes: ['epub', 'pdf', 'mobi', 'azw3'] | ||||
| } | ||||
| 
 | ||||
| module.exports = globals | ||||
|  | ||||
							
								
								
									
										404
									
								
								server/utils/mobi.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										404
									
								
								server/utils/mobi.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,404 @@ | ||||
| function ab2str(buf) { | ||||
|   if (buf instanceof ArrayBuffer) { | ||||
|     buf = new Uint8Array(buf); | ||||
|   } | ||||
|   return new TextDecoder("utf-8").decode(buf); | ||||
| } | ||||
| 
 | ||||
| var domParser = new DOMParser(); | ||||
| 
 | ||||
| class Buffer { | ||||
|   constructor(capacity) { | ||||
|     this.capacity = capacity; | ||||
|     this.fragment_list = []; | ||||
|     this.cur_fragment = new Fragment(capacity); | ||||
|     this.fragment_list.push(this.cur_fragment); | ||||
|   } | ||||
|   write(byte) { | ||||
|     var result = this.cur_fragment.write(byte); | ||||
|     if (!result) { | ||||
|       this.cur_fragment = new Fragment(this.capacity); | ||||
|       this.fragment_list.push(this.cur_fragment); | ||||
|       this.cur_fragment.write(byte); | ||||
|     } | ||||
|   } | ||||
|   get(idx) { | ||||
|     var fi = 0; | ||||
|     while (fi < this.fragment_list.length) { | ||||
|       var frag = this.fragment_list[fi]; | ||||
|       if (idx < frag.size) { | ||||
|         return frag.get(idx); | ||||
|       } | ||||
|       idx -= frag.size; | ||||
|       fi += 1; | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
|   size() { | ||||
|     var s = 0; | ||||
|     for (var i = 0; i < this.fragment_list.length; i++) { | ||||
|       s += this.fragment_list[i].size; | ||||
|     } | ||||
|     return s; | ||||
|   } | ||||
|   shrink() { | ||||
|     var total_buffer = new Uint8Array(this.size()); | ||||
|     var offset = 0; | ||||
|     for (var i = 0; i < this.fragment_list.length; i++) { | ||||
|       var frag = this.fragment_list[i]; | ||||
|       if (frag.full()) { | ||||
|         total_buffer.set(frag.buffer, offset); | ||||
|       } else { | ||||
|         total_buffer.set(frag.buffer.slice(0, frag.size), offset); | ||||
|       } | ||||
|       offset += frag.size; | ||||
|     } | ||||
|     return total_buffer; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| var combine_uint8array = function (buffers) { | ||||
|   var total_size = 0; | ||||
|   for (var i = 0; i < buffers.length; i++) { | ||||
|     var buffer = buffers[i]; | ||||
|     total_size += buffer.length; | ||||
|   } | ||||
|   var total_buffer = new Uint8Array(total_size); | ||||
|   var offset = 0; | ||||
|   for (var i = 0; i < buffers.length; i++) { | ||||
|     var buffer = buffers[i]; | ||||
|     total_buffer.set(buffer, offset); | ||||
|     offset += buffer.length; | ||||
|   } | ||||
|   return total_buffer; | ||||
| } | ||||
| 
 | ||||
| class Fragment { | ||||
|   constructor(capacity) { | ||||
|     this.buffer = new Uint8Array(capacity); | ||||
|     this.capacity = capacity; | ||||
|     this.size = 0; | ||||
|   } | ||||
| 
 | ||||
|   write(byte) { | ||||
|     if (this.size >= this.capacity) { | ||||
|       return false; | ||||
|     } | ||||
|     this.buffer[this.size] = byte; | ||||
|     this.size += 1; | ||||
|     return true; | ||||
|   } | ||||
|   full() { | ||||
|     return this.size === this.capacity; | ||||
|   } | ||||
|   get(idx) { | ||||
|     return this.buffer[idx]; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| var uncompression_lz77 = function (data) { | ||||
|   var length = data.length; | ||||
|   var offset = 0;   // Current offset into data
 | ||||
|   var buffer = new Buffer(data.length); | ||||
| 
 | ||||
|   while (offset < length) { | ||||
|     var char = data[offset]; | ||||
|     offset += 1; | ||||
| 
 | ||||
|     if (char == 0) { | ||||
|       buffer.write(char); | ||||
|     } else if (char <= 8) { | ||||
|       for (var i = offset; i < offset + char; i++) { | ||||
|         buffer.write(data[i]); | ||||
|       } | ||||
|       offset += char; | ||||
|     } else if (char <= 0x7f) { | ||||
|       buffer.write(char); | ||||
|     } else if (char <= 0xbf) { | ||||
|       var next = data[offset]; | ||||
|       offset += 1; | ||||
|       var distance = ((char << 8 | next) >> 3) & 0x7ff; | ||||
|       var lz_length = (next & 0x7) + 3; | ||||
| 
 | ||||
|       var buffer_size = buffer.size(); | ||||
|       for (var i = 0; i < lz_length; i++) { | ||||
|         buffer.write(buffer.get(buffer_size - distance)) | ||||
|         buffer_size += 1; | ||||
|       } | ||||
|     } else { | ||||
|       buffer.write(32); | ||||
|       buffer.write(char ^ 0x80); | ||||
|     } | ||||
|   } | ||||
|   return buffer; | ||||
| }; | ||||
| 
 | ||||
| class MobiFile { | ||||
|   constructor(data) { | ||||
|     this.view = new DataView(data); | ||||
|     this.buffer = this.view.buffer; | ||||
|     this.offset = 0; | ||||
|     this.header = null; | ||||
|   } | ||||
| 
 | ||||
|   parse() { | ||||
| 
 | ||||
|   } | ||||
| 
 | ||||
|   getUint8() { | ||||
|     var v = this.view.getUint8(this.offset); | ||||
|     this.offset += 1; | ||||
|     return v; | ||||
|   } | ||||
| 
 | ||||
|   getUint16() { | ||||
|     var v = this.view.getUint16(this.offset); | ||||
|     this.offset += 2; | ||||
|     return v; | ||||
|   } | ||||
| 
 | ||||
|   getUint32() { | ||||
|     var v = this.view.getUint32(this.offset); | ||||
|     this.offset += 4; | ||||
|     return v; | ||||
|   } | ||||
| 
 | ||||
|   getStr(size) { | ||||
|     var v = ab2str(this.buffer.slice(this.offset, this.offset + size)); | ||||
|     this.offset += size; | ||||
|     return v; | ||||
|   } | ||||
| 
 | ||||
|   skip(size) { | ||||
|     this.offset += size; | ||||
|   } | ||||
| 
 | ||||
|   setoffset(_of) { | ||||
|     this.offset = _of; | ||||
|   } | ||||
| 
 | ||||
|   get_record_extrasize(data, flags) { | ||||
|     var pos = data.length - 1; | ||||
|     var extra = 0; | ||||
|     for (var i = 15; i > 0; i--) { | ||||
|       if (flags & (1 << i)) { | ||||
|         var res = this.buffer_get_varlen(data, pos); | ||||
|         var size = res[0]; | ||||
|         var l = res[1]; | ||||
|         pos = res[2]; | ||||
|         pos -= size - l; | ||||
|         extra += size; | ||||
|       } | ||||
|     } | ||||
|     if (flags & 1) { | ||||
|       var a = data[pos]; | ||||
|       extra += (a & 0x3) + 1; | ||||
|     } | ||||
|     return extra; | ||||
|   } | ||||
| 
 | ||||
|   // data should be uint8array
 | ||||
|   buffer_get_varlen(data, pos) { | ||||
|     var l = 0; | ||||
|     var size = 0; | ||||
|     var byte_count = 0; | ||||
|     var mask = 0x7f; | ||||
|     var stop_flag = 0x80; | ||||
|     var shift = 0; | ||||
|     for (var i = 0; ; i++) { | ||||
|       var byte = data[pos]; | ||||
|       size |= (byte & mask) << shift; | ||||
|       shift += 7; | ||||
|       l += 1; | ||||
|       byte_count += 1; | ||||
|       pos -= 1; | ||||
| 
 | ||||
|       var to_stop = byte & stop_flag; | ||||
|       if (byte_count >= 4 || to_stop > 0) { | ||||
|         break; | ||||
|       } | ||||
|     } | ||||
|     return [size, l, pos]; | ||||
|   } | ||||
| 
 | ||||
|   read_text() { | ||||
|     var text_end = this.palm_header.record_count; | ||||
|     var buffers = []; | ||||
|     for (var i = 1; i <= text_end; i++) { | ||||
|       buffers.push(this.read_text_record(i)); | ||||
|     } | ||||
|     var all = combine_uint8array(buffers) | ||||
|     return ab2str(all); | ||||
|   } | ||||
| 
 | ||||
|   read_text_record(i) { | ||||
|     var flags = this.mobi_header.extra_flags; | ||||
|     var begin = this.reclist[i].offset; | ||||
|     var end = this.reclist[i + 1].offset; | ||||
| 
 | ||||
|     var data = new Uint8Array(this.buffer.slice(begin, end)) | ||||
|     var ex = this.get_record_extrasize(data, flags); | ||||
| 
 | ||||
|     data = new Uint8Array(this.buffer.slice(begin, end - ex)); | ||||
|     if (this.palm_header.compression === 2) { | ||||
|       var buffer = uncompression_lz77(data); | ||||
|       return buffer.shrink(); | ||||
|     } else { | ||||
|       return data; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   read_image(idx) { | ||||
|     var first_image_idx = this.mobi_header.first_image_idx; | ||||
|     var begin = this.reclist[first_image_idx + idx].offset; | ||||
|     var end = this.reclist[first_image_idx + idx + 1].offset; | ||||
|     var data = new Uint8Array(this.buffer.slice(begin, end)) | ||||
|     return new Blob([data.buffer]); | ||||
|   } | ||||
| 
 | ||||
|   load() { | ||||
|     this.header = this.load_pdbheader(); | ||||
|     this.reclist = this.load_reclist(); | ||||
|     this.load_record0(); | ||||
|   } | ||||
| 
 | ||||
|   load_pdbheader() { | ||||
|     var header = {}; | ||||
|     header.name = this.getStr(32); | ||||
|     header.attr = this.getUint16(); | ||||
|     header.version = this.getUint16(); | ||||
|     header.ctime = this.getUint32(); | ||||
|     header.mtime = this.getUint32(); | ||||
|     header.btime = this.getUint32(); | ||||
|     header.mod_num = this.getUint32(); | ||||
|     header.appinfo_offset = this.getUint32(); | ||||
|     header.sortinfo_offset = this.getUint32(); | ||||
|     header.type = this.getStr(4); | ||||
|     header.creator = this.getStr(4); | ||||
|     header.uid = this.getUint32(); | ||||
|     header.next_rec = this.getUint32(); | ||||
|     header.record_num = this.getUint16(); | ||||
| 
 | ||||
|     return header; | ||||
|   } | ||||
| 
 | ||||
|   load_reclist() { | ||||
|     var reclist = []; | ||||
|     for (var i = 0; i < this.header.record_num; i++) { | ||||
|       var record = {}; | ||||
|       record.offset = this.getUint32(); | ||||
|       // TODO(zz) change 
 | ||||
|       record.attr = this.getUint32(); | ||||
|       reclist.push(record); | ||||
|     } | ||||
|     return reclist; | ||||
|   } | ||||
|   load_record0() { | ||||
|     this.palm_header = this.load_record0_header(); | ||||
|     this.mobi_header = this.load_mobi_header(); | ||||
|   } | ||||
| 
 | ||||
|   load_record0_header() { | ||||
|     var p_header = {}; | ||||
|     var first_record = this.reclist[0]; | ||||
|     this.setoffset(first_record.offset); | ||||
| 
 | ||||
|     p_header.compression = this.getUint16(); | ||||
|     this.skip(2); | ||||
|     p_header.text_length = this.getUint32(); | ||||
|     p_header.record_count = this.getUint16(); | ||||
|     p_header.record_size = this.getUint16(); | ||||
|     p_header.encryption_type = this.getUint16(); | ||||
|     this.skip(2); | ||||
| 
 | ||||
|     return p_header; | ||||
|   } | ||||
| 
 | ||||
|   load_mobi_header() { | ||||
|     var mobi_header = {}; | ||||
| 
 | ||||
|     var start_offset = this.offset; | ||||
| 
 | ||||
|     mobi_header.identifier = this.getUint32(); | ||||
|     mobi_header.header_length = this.getUint32(); | ||||
|     mobi_header.mobi_type = this.getUint32(); | ||||
|     mobi_header.text_encoding = this.getUint32(); | ||||
|     mobi_header.uid = this.getUint32(); | ||||
|     mobi_header.generator_version = this.getUint32(); | ||||
| 
 | ||||
|     this.skip(40); | ||||
| 
 | ||||
|     mobi_header.first_nonbook_index = this.getUint32(); | ||||
|     mobi_header.full_name_offset = this.getUint32(); | ||||
|     mobi_header.full_name_length = this.getUint32(); | ||||
| 
 | ||||
|     mobi_header.language = this.getUint32(); | ||||
|     mobi_header.input_language = this.getUint32(); | ||||
|     mobi_header.output_language = this.getUint32(); | ||||
|     mobi_header.min_version = this.getUint32(); | ||||
|     mobi_header.first_image_idx = this.getUint32(); | ||||
| 
 | ||||
|     mobi_header.huff_rec_index = this.getUint32(); | ||||
|     mobi_header.huff_rec_count = this.getUint32(); | ||||
|     mobi_header.datp_rec_index = this.getUint32(); | ||||
|     mobi_header.datp_rec_count = this.getUint32(); | ||||
| 
 | ||||
|     mobi_header.exth_flags = this.getUint32(); | ||||
| 
 | ||||
|     this.skip(36); | ||||
| 
 | ||||
|     mobi_header.drm_offset = this.getUint32(); | ||||
|     mobi_header.drm_count = this.getUint32(); | ||||
|     mobi_header.drm_size = this.getUint32(); | ||||
|     mobi_header.drm_flags = this.getUint32(); | ||||
| 
 | ||||
|     this.skip(8); | ||||
| 
 | ||||
|     // TODO (zz) fdst_index
 | ||||
|     this.skip(4); | ||||
| 
 | ||||
|     this.skip(46); | ||||
| 
 | ||||
|     mobi_header.extra_flags = this.getUint16(); | ||||
| 
 | ||||
|     this.setoffset(start_offset + mobi_header.header_length) | ||||
| 
 | ||||
|     return mobi_header; | ||||
|   } | ||||
|   load_exth_header() { | ||||
|     // TODO
 | ||||
|     return {}; | ||||
|   } | ||||
| 
 | ||||
|   render_to(id) { | ||||
|     this.load(); | ||||
|     var content = this.read_text(); | ||||
| 
 | ||||
|     var bookDom = document.getElementById(id); | ||||
|     while (bookDom.firstChild) { | ||||
|       bookDom.removeChild(bookDom.firstChild); | ||||
|     } | ||||
| 
 | ||||
|     var bookDoc = domParser.parseFromString(content, "text/html"); | ||||
|     bookDoc.body.childNodes.forEach(function (x) { | ||||
|       if (x instanceof Element) { | ||||
|         bookDom.appendChild(x); | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     var imgDoms = bookDom.getElementsByTagName("img"); | ||||
|     for (var i = 0; i < imgDoms.length; i++) { | ||||
|       this.render_image(imgDoms, i); | ||||
|     } | ||||
|   } | ||||
|   render_image(imgDoms, i) { | ||||
|     var imgDom = imgDoms[i]; | ||||
|     var idx = +imgDom.getAttribute("recindex"); | ||||
|     var blob = this.read_image(idx - 1); | ||||
|     var imgReader = new FileReader(); | ||||
|     imgReader.onload = function (e) { | ||||
|       imgDom.src = e.target.result; | ||||
|     }; | ||||
|     imgReader.readAsDataURL(blob); | ||||
|   } | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user