mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-02-04 01:09:40 +00:00
narrator filter, no series filter, full paths toggle, book landing page details, new sans font, update query string on filter/sort, persist experimental feature flag, batch edit redirect bug, upload file permissions and owner
This commit is contained in:
parent
75aede914f
commit
f752c19418
36 changed files with 454 additions and 230 deletions
|
|
@ -724,55 +724,55 @@ class Scanner {
|
|||
return libraryScanResults
|
||||
}
|
||||
|
||||
async scanCovers() {
|
||||
var audiobooksNeedingCover = this.audiobooks.filter(ab => !ab.cover && ab.author)
|
||||
var found = 0
|
||||
var notFound = 0
|
||||
var failed = 0
|
||||
// async scanCovers() {
|
||||
// var audiobooksNeedingCover = this.audiobooks.filter(ab => !ab.cover && ab.author)
|
||||
// var found = 0
|
||||
// var notFound = 0
|
||||
// var failed = 0
|
||||
|
||||
for (let i = 0; i < audiobooksNeedingCover.length; i++) {
|
||||
var audiobook = audiobooksNeedingCover[i]
|
||||
var options = {
|
||||
titleDistance: 2,
|
||||
authorDistance: 2
|
||||
}
|
||||
var results = await this.bookFinder.findCovers('openlibrary', audiobook.title, audiobook.author, options)
|
||||
if (results.length) {
|
||||
Logger.debug(`[Scanner] Found best cover for "${audiobook.title}"`)
|
||||
var coverUrl = results[0]
|
||||
var result = await this.coverController.downloadCoverFromUrl(audiobook, coverUrl)
|
||||
if (result.error) {
|
||||
failed++
|
||||
} else {
|
||||
found++
|
||||
await this.db.updateAudiobook(audiobook)
|
||||
this.emitter('audiobook_updated', audiobook.toJSONMinified())
|
||||
}
|
||||
} else {
|
||||
notFound++
|
||||
}
|
||||
// for (let i = 0; i < audiobooksNeedingCover.length; i++) {
|
||||
// var audiobook = audiobooksNeedingCover[i]
|
||||
// var options = {
|
||||
// titleDistance: 2,
|
||||
// authorDistance: 2
|
||||
// }
|
||||
// var results = await this.bookFinder.findCovers('openlibrary', audiobook.title, audiobook.author, options)
|
||||
// if (results.length) {
|
||||
// Logger.debug(`[Scanner] Found best cover for "${audiobook.title}"`)
|
||||
// var coverUrl = results[0]
|
||||
// var result = await this.coverController.downloadCoverFromUrl(audiobook, coverUrl)
|
||||
// if (result.error) {
|
||||
// failed++
|
||||
// } else {
|
||||
// found++
|
||||
// await this.db.updateAudiobook(audiobook)
|
||||
// this.emitter('audiobook_updated', audiobook.toJSONMinified())
|
||||
// }
|
||||
// } else {
|
||||
// notFound++
|
||||
// }
|
||||
|
||||
var progress = Math.round(100 * (i + 1) / audiobooksNeedingCover.length)
|
||||
this.emitter('scan_progress', {
|
||||
scanType: 'covers',
|
||||
progress: {
|
||||
total: audiobooksNeedingCover.length,
|
||||
done: i + 1,
|
||||
progress
|
||||
}
|
||||
})
|
||||
// var progress = Math.round(100 * (i + 1) / audiobooksNeedingCover.length)
|
||||
// this.emitter('scan_progress', {
|
||||
// scanType: 'covers',
|
||||
// progress: {
|
||||
// total: audiobooksNeedingCover.length,
|
||||
// done: i + 1,
|
||||
// progress
|
||||
// }
|
||||
// })
|
||||
|
||||
if (this.cancelScan) {
|
||||
this.cancelScan = false
|
||||
break
|
||||
}
|
||||
}
|
||||
return {
|
||||
found,
|
||||
notFound,
|
||||
failed
|
||||
}
|
||||
}
|
||||
// if (this.cancelScan) {
|
||||
// this.cancelScan = false
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// return {
|
||||
// found,
|
||||
// notFound,
|
||||
// failed
|
||||
// }
|
||||
// }
|
||||
|
||||
async saveMetadata(audiobookId) {
|
||||
if (audiobookId) {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ const { version } = require('../package.json')
|
|||
|
||||
// Utils
|
||||
const { ScanResult } = require('./utils/constants')
|
||||
const filePerms = require('./utils/filePerms')
|
||||
const Logger = require('./Logger')
|
||||
|
||||
// Classes
|
||||
|
|
@ -26,16 +27,18 @@ const CoverController = require('./CoverController')
|
|||
|
||||
|
||||
class Server {
|
||||
constructor(PORT, CONFIG_PATH, METADATA_PATH, AUDIOBOOK_PATH) {
|
||||
constructor(PORT, UID, GID, CONFIG_PATH, METADATA_PATH, AUDIOBOOK_PATH) {
|
||||
this.Port = PORT
|
||||
this.Uid = isNaN(UID) ? 0 : Number(UID)
|
||||
this.Gid = isNaN(GID) ? 0 : Number(GID)
|
||||
this.Host = '0.0.0.0'
|
||||
this.ConfigPath = Path.normalize(CONFIG_PATH)
|
||||
this.AudiobookPath = Path.normalize(AUDIOBOOK_PATH)
|
||||
this.MetadataPath = Path.normalize(METADATA_PATH)
|
||||
|
||||
fs.ensureDirSync(CONFIG_PATH)
|
||||
fs.ensureDirSync(METADATA_PATH)
|
||||
fs.ensureDirSync(AUDIOBOOK_PATH)
|
||||
fs.ensureDirSync(CONFIG_PATH, 0o774)
|
||||
fs.ensureDirSync(METADATA_PATH, 0o774)
|
||||
fs.ensureDirSync(AUDIOBOOK_PATH, 0o774)
|
||||
|
||||
this.db = new Db(this.ConfigPath, this.AudiobookPath)
|
||||
this.auth = new Auth(this.db)
|
||||
|
|
@ -217,7 +220,6 @@ class Server {
|
|||
|
||||
// Scanning
|
||||
socket.on('scan', this.scan.bind(this))
|
||||
socket.on('scan_covers', this.scanCovers.bind(this))
|
||||
socket.on('cancel_scan', this.cancelScan.bind(this))
|
||||
socket.on('scan_audiobook', (audiobookId) => this.scanAudiobook(socket, audiobookId))
|
||||
socket.on('save_metadata', (audiobookId) => this.saveMetadata(socket, audiobookId))
|
||||
|
|
@ -280,16 +282,6 @@ class Server {
|
|||
socket.emit('audiobook_scan_complete', scanResultName)
|
||||
}
|
||||
|
||||
async scanCovers() {
|
||||
Logger.info('[Server] Start cover scan')
|
||||
this.isScanningCovers = true
|
||||
// this.emitter('scan_start', 'covers')
|
||||
var results = await this.scanner.scanCovers()
|
||||
this.isScanningCovers = false
|
||||
// this.emitter('scan_complete', { scanType: 'covers', results })
|
||||
Logger.info('[Server] Cover scan complete')
|
||||
}
|
||||
|
||||
cancelScan(id) {
|
||||
Logger.debug('[Server] Cancel scan', id)
|
||||
this.scanner.cancelLibraryScan[id] = true
|
||||
|
|
@ -359,6 +351,9 @@ class Server {
|
|||
return res.status(500).error(`Invalid post data`)
|
||||
}
|
||||
|
||||
// For setting permissions recursively
|
||||
var firstDirPath = Path.join(folder.fullPath, author)
|
||||
|
||||
var outputDirectory = ''
|
||||
if (series && series.length && series !== 'null') {
|
||||
outputDirectory = Path.join(folder.fullPath, author, series, title)
|
||||
|
|
@ -373,16 +368,24 @@ class Server {
|
|||
}
|
||||
|
||||
await fs.ensureDir(outputDirectory)
|
||||
|
||||
Logger.info(`Uploading ${files.length} files to`, outputDirectory)
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
var file = files[i]
|
||||
|
||||
var path = Path.join(outputDirectory, file.name)
|
||||
await file.mv(path).catch((error) => {
|
||||
await file.mv(path).then(() => {
|
||||
return true
|
||||
}).catch((error) => {
|
||||
Logger.error('Failed to move file', path, error)
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
Logger.info(`[Server] Setting owner/perms for first dir "${firstDirPath}"`)
|
||||
await filePerms(firstDirPath, 0o774, this.Uid, this.Gid)
|
||||
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -498,13 +498,13 @@ class Audiobook {
|
|||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
// If reader.txt is new or forcing rescan then read it and update narrarator (will overwrite)
|
||||
// If reader.txt is new or forcing rescan then read it and update narrator (will overwrite)
|
||||
var readerTxt = newOtherFiles.find(file => file.filename === 'reader.txt')
|
||||
if (readerTxt && (!alreadyHasReaderTxt || forceRescan)) {
|
||||
var newReader = await readTextFile(readerTxt.fullPath)
|
||||
if (newReader) {
|
||||
Logger.debug(`[Audiobook] Sync Other File reader.txt: ${newReader}`)
|
||||
this.update({ book: { narrarator: newReader } })
|
||||
this.update({ book: { narrator: newReader } })
|
||||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
|
|
@ -712,8 +712,8 @@ class Audiobook {
|
|||
}
|
||||
var readerText = await this.fetchTextFromTextFile('reader.txt')
|
||||
if (readerText) {
|
||||
Logger.debug(`[Audiobook] "${this.title}" found reader.txt updating narrarator with "${readerText}"`)
|
||||
bookUpdatePayload.narrarator = readerText
|
||||
Logger.debug(`[Audiobook] "${this.title}" found reader.txt updating narrator with "${readerText}"`)
|
||||
bookUpdatePayload.narrator = readerText
|
||||
}
|
||||
if (Object.keys(bookUpdatePayload).length) {
|
||||
return this.update({ book: bookUpdatePayload })
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class Book {
|
|||
this.author = null
|
||||
this.authorFL = null
|
||||
this.authorLF = null
|
||||
this.narrarator = null
|
||||
this.narrator = null
|
||||
this.series = null
|
||||
this.volumeNumber = null
|
||||
this.publishYear = null
|
||||
|
|
@ -35,7 +35,7 @@ class Book {
|
|||
|
||||
get _title() { return this.title || '' }
|
||||
get _subtitle() { return this.subtitle || '' }
|
||||
get _narrarator() { return this.narrarator || '' }
|
||||
get _narrator() { return this.narrator || '' }
|
||||
get _author() { return this.author || '' }
|
||||
get _series() { return this.series || '' }
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ class Book {
|
|||
this.author = book.author
|
||||
this.authorFL = book.authorFL || null
|
||||
this.authorLF = book.authorLF || null
|
||||
this.narrarator = book.narrarator || null
|
||||
this.narrator = book.narrator || book.narrarator || null // Mispelled initially... need to catch those
|
||||
this.series = book.series
|
||||
this.volumeNumber = book.volumeNumber || null
|
||||
this.publishYear = book.publishYear
|
||||
|
|
@ -75,7 +75,7 @@ class Book {
|
|||
author: this.author,
|
||||
authorFL: this.authorFL,
|
||||
authorLF: this.authorLF,
|
||||
narrarator: this.narrarator,
|
||||
narrator: this.narrator,
|
||||
series: this.series,
|
||||
volumeNumber: this.volumeNumber,
|
||||
publishYear: this.publishYear,
|
||||
|
|
@ -115,7 +115,7 @@ class Book {
|
|||
this.title = data.title || null
|
||||
this.subtitle = data.subtitle || null
|
||||
this.author = data.author || null
|
||||
this.narrarator = data.narrarator || null
|
||||
this.narrator = data.narrator || data.narrarator || null
|
||||
this.series = data.series || null
|
||||
this.volumeNumber = data.volumeNumber || null
|
||||
this.publishYear = data.publishYear || null
|
||||
|
|
@ -221,7 +221,7 @@ class Book {
|
|||
const MetadataMapArray = [
|
||||
{
|
||||
tag: 'tagComposer',
|
||||
key: 'narrarator'
|
||||
key: 'narrator'
|
||||
},
|
||||
{
|
||||
tag: 'tagDescription',
|
||||
|
|
|
|||
85
server/utils/filePerms.js
Normal file
85
server/utils/filePerms.js
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
const fs = require('fs-extra')
|
||||
const Path = require('path')
|
||||
const Logger = require('../Logger')
|
||||
|
||||
// Modified from:
|
||||
// https://github.com/isaacs/chmodr/blob/master/chmodr.js
|
||||
|
||||
// If a party has r, add x
|
||||
// so that dirs are listable
|
||||
const dirMode = mode => {
|
||||
if (mode & 0o400)
|
||||
mode |= 0o100
|
||||
if (mode & 0o40)
|
||||
mode |= 0o10
|
||||
if (mode & 0o4)
|
||||
mode |= 0o1
|
||||
return mode
|
||||
}
|
||||
|
||||
const chmodrKid = (p, child, mode, uid, gid, cb) => {
|
||||
if (typeof child === 'string')
|
||||
return fs.lstat(Path.resolve(p, child), (er, stats) => {
|
||||
if (er)
|
||||
return cb(er)
|
||||
stats.name = child
|
||||
chmodrKid(p, stats, mode, uid, gid, cb)
|
||||
})
|
||||
|
||||
if (child.isDirectory()) {
|
||||
chmodr(Path.resolve(p, child.name), mode, uid, gid, er => {
|
||||
if (er)
|
||||
return cb(er)
|
||||
|
||||
var _path = Path.resolve(p, child.name)
|
||||
fs.chmod(_path, dirMode(mode)).then(() => {
|
||||
fs.chown(_path, uid, gid, cb)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
var _path = Path.resolve(p, child.name)
|
||||
fs.chmod(_path, mode).then(() => {
|
||||
fs.chown(_path, uid, gid, cb)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const chmodr = (p, mode, uid, gid, cb) => {
|
||||
fs.readdir(p, { withFileTypes: true }, (er, children) => {
|
||||
// any error other than ENOTDIR means it's not readable, or
|
||||
// doesn't exist. give up.
|
||||
if (er && er.code !== 'ENOTDIR') return cb(er)
|
||||
if (er) {
|
||||
return fs.chmod(p, mode).then(() => {
|
||||
fs.chown(p, uid, gid, cb)
|
||||
})
|
||||
}
|
||||
if (!children.length) {
|
||||
return fs.chmod(p, dirMode(mode)).then(() => {
|
||||
fs.chown(p, uid, gid, cb)
|
||||
})
|
||||
}
|
||||
|
||||
let len = children.length
|
||||
let errState = null
|
||||
const then = er => {
|
||||
if (errState) return
|
||||
if (er) return cb(errState = er)
|
||||
if (--len === 0) {
|
||||
return fs.chmod(p, dirMode(mode)).then(() => {
|
||||
fs.chown(p, uid, gid, cb)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
children.forEach(child => chmodrKid(p, child, mode, uid, gid, then))
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = (p, mode, uid, gid) => {
|
||||
return new Promise((resolve) => {
|
||||
Logger.debug(`[FilePerms] Setting permission "${mode}" for uid ${uid} and gid ${gid} | "${p}"`)
|
||||
chmodr(p, mode, uid, gid, resolve)
|
||||
})
|
||||
}
|
||||
|
|
@ -75,4 +75,14 @@ function secondsToTimestamp(seconds) {
|
|||
}
|
||||
return `${_hours}:${_minutes.toString().padStart(2, '0')}:${_seconds.toString().padStart(2, '0')}`
|
||||
}
|
||||
module.exports.secondsToTimestamp = secondsToTimestamp
|
||||
module.exports.secondsToTimestamp = secondsToTimestamp
|
||||
|
||||
function setFileOwner(path, uid, gid) {
|
||||
try {
|
||||
return fs.chown(path, uid, gid).then(() => true)
|
||||
} catch (err) {
|
||||
console.error('Failed set file owner', err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
module.exports.setFileOwner = setFileOwner
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ async function generate(audiobook, nfoFilename = 'metadata.nfo') {
|
|||
'Title': book.title,
|
||||
'Subtitle': book.subtitle,
|
||||
'Author': book.author,
|
||||
'Narrator': book.narrarator,
|
||||
'Narrator': book.narrator,
|
||||
'Series': book.series,
|
||||
'Volume Number': book.volumeNumber,
|
||||
'Publish Year': book.publishYear,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue