Compare commits

...

2 Commits

16 changed files with 900 additions and 25 deletions
+405
View 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
+47 -5
View File
@@ -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 -1
View File
@@ -5,7 +5,7 @@
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
</div>
</template>
<div class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 200px; max-height: 80vh">
<div v-if="show" class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 200px; max-height: 80vh">
<div v-if="!showAddLibrary" class="w-full h-full flex flex-col justify-center px-4">
<div class="flex items-center mb-4">
<p>{{ libraries.length }} Libraries</p>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
"version": "1.4.4",
"version": "1.4.6",
"description": "Audiobook manager and player",
"main": "index.js",
"scripts": {
+1 -1
View File
@@ -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>
+11 -4
View File
@@ -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')
+2 -2
View File
@@ -129,9 +129,9 @@ export default {
title: null,
author: null,
series: null,
acceptedAudioFormats: ['.mp3', '.m4b', '.m4a', '.flac', '.opus'],
acceptedAudioFormats: ['.mp3', '.m4b', '.m4a', '.flac', '.opus', '.mp4'],
acceptedImageFormats: ['.png', '.jpg', '.jpeg', '.webp'],
inputAccept: '.png, .jpg, .jpeg, .webp, .mp3, .m4b, .m4a, .flac, .opus',
inputAccept: '.png, .jpg, .jpeg, .webp, .mp3, .m4b, .m4a, .flac, .opus, .mp4',
isDragOver: false,
showUploader: true,
validAudioFiles: [],
-1
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf",
"version": "1.4.4",
"version": "1.4.6",
"description": "Self-hosted audiobook server for managing and playing audiobooks",
"main": "index.js",
"scripts": {
+7 -3
View File
@@ -692,7 +692,7 @@ class ApiController {
var path = Path.join(relpath, dirname)
var isDir = (await fs.lstat(fullPath)).isDirectory()
if (isDir && !excludedDirs.includes(dirname)) {
if (isDir && !excludedDirs.includes(path) && dirname !== 'node_modules') {
return {
path,
dirname,
@@ -713,12 +713,16 @@ class ApiController {
}
async getFileSystemPaths(req, res) {
var excludedDirs = ['node_modules', 'client', 'server', '.git', 'static', 'build', 'dist', 'metadata', 'config', 'sys', 'proc']
var excludedDirs = ['node_modules', 'client', 'server', '.git', 'static', 'build', 'dist', 'metadata', 'config', 'sys', 'proc'].map(dirname => {
return Path.sep + dirname
})
// Do not include existing mapped library paths in response
this.db.libraries.forEach(lib => {
lib.folders.forEach((folder) => {
excludedDirs.push(Path.basename(folder.fullPath))
var dir = folder.fullPath
if (dir.includes(global.appRoot)) dir = dir.replace(global.appRoot, '')
excludedDirs.push(dir)
})
})
+7 -2
View File
@@ -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,
@@ -456,11 +460,12 @@ class Audiobook {
var current_index = 1
var missingParts = []
for (let i = 0; i < this.tracks.length; i++) {
var _track = this.tracks[i]
if (_track.index > current_index) {
var num_parts_missing = _track.index - current_index
for (let x = 0; x < num_parts_missing; x++) {
for (let x = 0; x < num_parts_missing && x < 9999; x++) {
missingParts.push(current_index + x)
}
}
-1
View File
@@ -67,7 +67,6 @@ class Backup {
this.filename = this.id + '.audiobookshelf'
this.path = Path.join('backups', this.filename)
this.fullPath = Path.join(this.backupDirPath, this.filename)
console.log('Backup fullpath', this.fullPath)
this.createdAt = Date.now()
}
+4 -1
View File
@@ -12,6 +12,7 @@ function getDefaultAudioStream(audioStreams) {
}
async function scan(path) {
Logger.debug(`Scanning path "${path}"`)
var probeData = await prober(path)
if (!probeData || !probeData.audio_streams || !probeData.audio_streams.length) {
return {
@@ -86,7 +87,7 @@ function getTrackNumberFromFilename(title, author, series, publishYear, filename
// Remove eg. "disc 1" from path
partbasename = partbasename.replace(/ disc \d\d? /i, '')
var numbersinpath = partbasename.match(/\d+/g)
var numbersinpath = partbasename.match(/\d{1,4}/g)
if (!numbersinpath) return null
var number = numbersinpath.length ? parseInt(numbersinpath[0]) : null
@@ -99,6 +100,8 @@ async function scanAudioFiles(audiobook, newAudioFiles) {
return
}
Logger.debug('[AudioFileScanner] Scanning audio files')
var tracks = []
var numDuplicateTracks = 0
var numInvalidTracks = 0
+2 -2
View File
@@ -1,7 +1,7 @@
const globals = {
SupportedImageTypes: ['png', 'jpg', 'jpeg', 'webp'],
SupportedAudioTypes: ['m4b', 'mp3', 'm4a', 'flac', 'opus'],
SupportedEbookTypes: ['epub', 'pdf', 'mobi']
SupportedAudioTypes: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'mp4'],
SupportedEbookTypes: ['epub', 'pdf', 'mobi', 'azw3']
}
module.exports = globals
+404
View 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);
}
}
+7
View File
@@ -1,4 +1,5 @@
const Path = require('path')
const fs = require('fs-extra')
const dir = require('node-dir')
const Logger = require('../Logger')
const { getIno } = require('./index')
@@ -120,6 +121,12 @@ async function scanRootDir(folder, serverSettings = {}) {
var folderPath = folder.fullPath
var parseSubtitle = !!serverSettings.scannerParseSubtitle
var pathExists = await fs.pathExists(folderPath)
if (!pathExists) {
Logger.error(`[scandir] Invalid folder path does not exist "${folderPath}"`)
return []
}
var pathdata = await getPaths(folderPath)
var filepaths = pathdata.files.map(filepath => {
return Path.normalize(filepath).replace(folderPath, '')