Update users table info #94, Reorder libraries in config #95, Use dropdown for library menu #96, update mobi reader

This commit is contained in:
advplyr 2021-10-12 20:07:42 -05:00
parent 9715c53332
commit cd6e99b4c3
30 changed files with 1361 additions and 557 deletions

View file

@ -30,6 +30,7 @@ class ApiController {
this.router.get('/find/:method', this.find.bind(this))
this.router.get('/libraries', this.getLibraries.bind(this))
this.router.patch('/libraries/order', this.reorderLibraries.bind(this))
this.router.get('/library/:id/search', this.searchLibrary.bind(this))
this.router.get('/library/:id', this.getLibrary.bind(this))
this.router.delete('/library/:id', this.deleteLibrary.bind(this))
@ -98,6 +99,36 @@ class ApiController {
res.json(libraries)
}
async reorderLibraries(req, res) {
if (!req.user || !req.user.isRoot) {
Logger.error('[ApiController] ReorderLibraries invalid user', req.user)
return res.sendStatus(401)
}
var orderdata = req.body
var hasUpdates = false
for (let i = 0; i < orderdata.length; i++) {
var library = this.db.libraries.find(lib => lib.id === orderdata[i].id)
if (!library) {
Logger.error(`[ApiController] Invalid library not found in reorder ${orderdata[i].id}`)
return res.sendStatus(500)
}
if (library.update({ displayOrder: orderdata[i].newOrder })) {
hasUpdates = true
await this.db.updateEntity('library', library)
}
}
if (hasUpdates) {
Logger.info(`[ApiController] Updated library display orders`)
} else {
Logger.info(`[ApiController] Library orders were up to date`)
}
var libraries = this.db.libraries.map(lib => lib.toJSON())
res.json(libraries)
}
searchLibrary(req, res) {
var library = this.db.libraries.find(lib => lib.id === req.params.id)
if (!library) {
@ -226,6 +257,7 @@ class ApiController {
}
var library = new Library()
newLibraryPayload.displayOrder = this.db.libraries.length + 1
library.setData(newLibraryPayload)
await this.db.insertEntity('library', library)
this.emitter('library_added', library.toJSON())

View file

@ -92,6 +92,10 @@ class Server {
})
}
authMiddleware(req, res, next) {
this.auth.authMiddleware(req, res, next)
}
async init() {
Logger.info('[Server] Init v' + version)
await this.streamManager.ensureStreamsDir()
@ -270,6 +274,8 @@ class Server {
Logger.info('[SOCKET] Unauth socket disconnected ' + socket.id)
delete this.clients[socket.id]
} else {
socket.broadcast.emit('user_offline', _client.user.toJSONForPublic(this.streamManager.streams))
const disconnectTime = Date.now() - _client.connected_at
Logger.info(`[SOCKET] Socket ${socket.id} disconnected from client "${_client.user.username}" after ${disconnectTime}ms`)
delete this.clients[socket.id]
@ -341,10 +347,6 @@ class Server {
return purged
}
authMiddleware(req, res, next) {
this.auth.authMiddleware(req, res, next)
}
async handleUpload(req, res) {
if (!req.user.canUpload) {
Logger.warn('User attempted to upload without permission', req.user)
@ -460,6 +462,11 @@ class Server {
}
}
socket.broadcast.emit('user_online', client.user.toJSONForPublic(this.streamManager.streams))
user.lastSeen = Date.now()
await this.db.updateEntity('user', user)
const initialPayload = {
serverSettings: this.serverSettings.toJSON(),
audiobookPath: this.AudiobookPath,

View file

@ -111,6 +111,8 @@ class StreamManager {
var stream = await this.openStream(client, audiobook)
this.db.updateUserStream(client.user.id, stream.id)
socket.broadcast.emit('user_stream_update', client.user.toJSONForPublic(this.streams))
}
async closeStreamRequest(socket) {
@ -126,6 +128,8 @@ class StreamManager {
client.user.stream = null
client.stream = null
this.db.updateUserStream(client.user.id, null)
socket.broadcast.emit('user_stream_update', client.user.toJSONForPublic(this.streams))
}
async openTestStream(StreamsPath, audiobookId) {

View file

@ -1,6 +1,7 @@
class AudiobookProgress {
constructor(progress) {
this.audiobookId = null
this.audiobookTitle = null
this.totalDuration = null // seconds
this.progress = null // 0 to 1
this.currentTime = null // seconds
@ -17,6 +18,7 @@ class AudiobookProgress {
toJSON() {
return {
audiobookId: this.audiobookId,
audiobookTitle: this.audiobookTitle,
totalDuration: this.totalDuration,
progress: this.progress,
currentTime: this.currentTime,
@ -29,6 +31,7 @@ class AudiobookProgress {
construct(progress) {
this.audiobookId = progress.audiobookId
this.audiobookTitle = progress.audiobookTitle || null
this.totalDuration = progress.totalDuration
this.progress = progress.progress
this.currentTime = progress.currentTime
@ -40,6 +43,7 @@ class AudiobookProgress {
updateFromStream(stream) {
this.audiobookId = stream.audiobookId
this.audiobookTitle = stream.audiobookTitle
this.totalDuration = stream.totalDuration
this.progress = stream.clientProgress
this.currentTime = stream.clientCurrentTime

View file

@ -5,6 +5,7 @@ class Library {
this.id = null
this.name = null
this.folders = []
this.displayOrder = 1
this.icon = 'database'
this.lastScan = 0
@ -25,6 +26,7 @@ class Library {
this.id = library.id
this.name = library.name
this.folders = (library.folders || []).map(f => new Folder(f))
this.displayOrder = library.displayOrder || 1
this.icon = library.icon || 'database'
this.createdAt = library.createdAt
@ -36,6 +38,7 @@ class Library {
id: this.id,
name: this.name,
folders: (this.folders || []).map(f => f.toJSON()),
displayOrder: this.displayOrder,
icon: this.icon,
createdAt: this.createdAt,
lastUpdate: this.lastUpdate
@ -59,6 +62,7 @@ class Library {
return newFolder
})
}
this.displayOrder = data.displayOrder || 1
this.icon = data.icon || 'database'
this.createdAt = Date.now()
this.lastUpdate = Date.now()
@ -70,6 +74,10 @@ class Library {
this.name = payload.name
hasUpdates = true
}
if (!isNaN(payload.displayOrder) && payload.displayOrder !== this.displayOrder) {
this.displayOrder = Number(payload.displayOrder)
hasUpdates = true
}
if (payload.folders) {
var newFolders = payload.folders.filter(f => !f.id)
var removedFolders = this.folders.filter(f => !payload.folders.find(_f => _f.id === f.id))

View file

@ -10,6 +10,7 @@ class User {
this.token = null
this.isActive = true
this.isLocked = false
this.lastSeen = null
this.createdAt = null
this.audiobooks = null
@ -78,6 +79,7 @@ class User {
audiobooks: this.audiobooksToJSON(),
isActive: this.isActive,
isLocked: this.isLocked,
lastSeen: this.lastSeen,
createdAt: this.createdAt,
settings: this.settings,
permissions: this.permissions
@ -94,12 +96,25 @@ class User {
audiobooks: this.audiobooksToJSON(),
isActive: this.isActive,
isLocked: this.isLocked,
lastSeen: this.lastSeen,
createdAt: this.createdAt,
settings: this.settings,
permissions: this.permissions
}
}
toJSONForPublic(streams) {
var stream = this.stream && streams ? streams.find(s => s.id === this.stream) : null
return {
id: this.id,
username: this.username,
type: this.type,
stream: stream ? stream.toJSON() : null,
lastSeen: this.lastSeen,
createdAt: this.createdAt
}
}
construct(user) {
this.id = user.id
this.username = user.username
@ -117,6 +132,7 @@ class User {
}
this.isActive = (user.isActive === undefined || user.type === 'root') ? true : !!user.isActive
this.isLocked = user.type === 'root' ? false : !!user.isLocked
this.lastSeen = user.lastSeen || null
this.createdAt = user.createdAt || Date.now()
this.settings = user.settings || this.getDefaultUserSettings()
this.permissions = user.permissions || this.getDefaultUserPermissions()

View file

@ -1,404 +0,0 @@
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);
}
}