Removing remaining legacy objects, remove njodb error for fileExists

This commit is contained in:
advplyr 2022-04-10 10:05:05 -05:00
parent 99e2ea228d
commit c60807f998
15 changed files with 18 additions and 2602 deletions

View File

@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
"version": "1.7.2",
"version": "1.7.3",
"description": "Audiobook manager and player",
"main": "index.js",
"scripts": {

View File

@ -1,6 +1,6 @@
{
"name": "audiobookshelf",
"version": "1.7.2",
"version": "1.7.3",
"description": "Self-hosted audiobook server for managing and playing audiobooks",
"main": "index.js",
"scripts": {

View File

@ -117,7 +117,7 @@ class Server {
// await this.streamManager.removeOrphanStreams()
await this.downloadManager.removeOrphanDownloads()
if (version.localeCompare('1.7.3') < 0) { // Old version data model migration
if (version.localeCompare('2.0.0') < 0) { // Old version data model migration
await dbMigration.migrate(this.db)
} else {
await this.db.init()

View File

@ -1,7 +1,6 @@
const fs = require('fs-extra')
const Logger = require('../Logger')
const Path = require('path')
const Author = require('../objects/legacy/Author')
const Audnexus = require('../providers/Audnexus')
const { downloadFile } = require('../utils/fileUtils')
@ -51,80 +50,5 @@ class AuthorFinder {
relPath
}
}
async createAuthor(payload) {
if (!payload || !payload.name) return null
var authorDir = Path.posix.join(this.AuthorPath, payload.name)
var relAuthorDir = Path.posix.join('/metadata', 'authors', payload.name)
if (payload.image && payload.image.startsWith('http')) {
await fs.ensureDir(authorDir)
var imageExtension = payload.image.toLowerCase().split('.').pop()
var ext = imageExtension === 'png' ? 'png' : 'jpg'
var filename = 'photo.' + ext
var outputPath = Path.posix.join(authorDir, filename)
var relPath = Path.posix.join(relAuthorDir, filename)
var success = await this.downloadImage(payload.image, outputPath)
if (!success) {
await fs.rmdir(authorDir).catch((error) => {
Logger.error(`[AuthorFinder] Failed to remove author dir`, authorDir, error)
})
payload.image = null
payload.imageFullPath = null
} else {
payload.image = relPath
payload.imageFullPath = outputPath
}
} else {
payload.image = null
payload.imageFullPath = null
}
var author = new Author()
author.setData(payload)
return author
}
async getAuthorByName(name, options = {}) {
var authorData = await this.findAuthorByName(name, options)
if (!authorData) return null
var authorDir = Path.posix.join(this.AuthorPath, authorData.name)
var relAuthorDir = Path.posix.join('/metadata', 'authors', authorData.name)
if (authorData.image) {
await fs.ensureDir(authorDir)
var imageExtension = authorData.image.toLowerCase().split('.').pop()
var ext = imageExtension === 'png' ? 'png' : 'jpg'
var filename = 'photo.' + ext
var outputPath = Path.posix.join(authorDir, filename)
var relPath = Path.posix.join(relAuthorDir, filename)
var success = await this.downloadImage(authorData.image, outputPath)
if (!success) {
await fs.rmdir(authorDir).catch((error) => {
Logger.error(`[AuthorFinder] Failed to remove author dir`, authorDir, error)
})
authorData.image = null
authorData.imageFullPath = null
} else {
authorData.image = relPath
authorData.imageFullPath = outputPath
}
} else {
authorData.image = null
authorData.imageFullPath = null
}
var author = new Author()
author.setData(authorData)
return author
}
}
module.exports = AuthorFinder

View File

@ -27,7 +27,7 @@ const max = (a, b) => {
const convertSize = (size) => {
const sizes = ["bytes", "KB", "MB", "GB"];
var index = Math.floor(Math.log2(size)/10);
var index = Math.floor(Math.log2(size) / 10);
if (index > 3) index = 3;
return Math.round(((size / Math.pow(1024, index)) + Number.EPSILON) * 100) / 100 + " " + sizes[index];
@ -38,7 +38,7 @@ const fileExists = async (a) => {
await promisify(access)(a, constants.F_OK);
return true;
} catch (error) {
console.error(error);
// console.error(error); file does not exist no need for error
return false;
}
}

View File

@ -1,243 +0,0 @@
const { isNullOrNaN } = require('../../utils/index')
const AudioFileMetadata = require('../metadata/AudioMetaTags')
class AudioFile {
constructor(data) {
this.index = null
this.ino = null
this.filename = null
this.ext = null
this.path = null
this.fullPath = null
this.mtimeMs = null
this.ctimeMs = null
this.birthtimeMs = null
this.addedAt = null
this.trackNumFromMeta = null
this.discNumFromMeta = null
this.trackNumFromFilename = null
this.discNumFromFilename = null
this.format = null
this.duration = null
this.size = null
this.bitRate = null
this.language = null
this.codec = null
this.timeBase = null
this.channels = null
this.channelLayout = null
this.chapters = []
this.embeddedCoverArt = null
// Tags scraped from the audio file
this.metadata = null
this.manuallyVerified = false
this.invalid = false
this.exclude = false
this.error = null
if (data) {
this.construct(data)
}
}
toJSON() {
return {
index: this.index,
ino: this.ino,
filename: this.filename,
ext: this.ext,
path: this.path,
fullPath: this.fullPath,
mtimeMs: this.mtimeMs,
ctimeMs: this.ctimeMs,
birthtimeMs: this.birthtimeMs,
addedAt: this.addedAt,
trackNumFromMeta: this.trackNumFromMeta,
discNumFromMeta: this.discNumFromMeta,
trackNumFromFilename: this.trackNumFromFilename,
discNumFromFilename: this.discNumFromFilename,
manuallyVerified: !!this.manuallyVerified,
invalid: !!this.invalid,
exclude: !!this.exclude,
error: this.error || null,
format: this.format,
duration: this.duration,
size: this.size,
bitRate: this.bitRate,
language: this.language,
codec: this.codec,
timeBase: this.timeBase,
channels: this.channels,
channelLayout: this.channelLayout,
chapters: this.chapters,
embeddedCoverArt: this.embeddedCoverArt,
metadata: this.metadata ? this.metadata.toJSON() : {}
}
}
construct(data) {
this.index = data.index
this.ino = data.ino
this.filename = data.filename
this.ext = data.ext
this.path = data.path
this.fullPath = data.fullPath
this.mtimeMs = data.mtimeMs || 0
this.ctimeMs = data.ctimeMs || 0
this.birthtimeMs = data.birthtimeMs || 0
this.addedAt = data.addedAt
this.manuallyVerified = !!data.manuallyVerified
this.invalid = !!data.invalid
this.exclude = !!data.exclude
this.error = data.error || null
this.trackNumFromMeta = data.trackNumFromMeta
this.discNumFromMeta = data.discNumFromMeta
this.trackNumFromFilename = data.trackNumFromFilename
if (data.cdNumFromFilename !== undefined) this.discNumFromFilename = data.cdNumFromFilename // TEMP:Support old var name
else this.discNumFromFilename = data.discNumFromFilename
this.format = data.format
this.duration = data.duration
this.size = data.size
this.bitRate = data.bitRate
this.language = data.language
this.codec = data.codec || null
this.timeBase = data.timeBase
this.channels = data.channels
this.channelLayout = data.channelLayout
this.chapters = data.chapters
this.embeddedCoverArt = data.embeddedCoverArt || null
// Old version of AudioFile used `tagAlbum` etc.
var isOldVersion = Object.keys(data).find(key => key.startsWith('tag'))
if (isOldVersion) {
this.metadata = new AudioFileMetadata(data)
} else {
this.metadata = new AudioFileMetadata(data.metadata || {})
}
}
// New scanner creates AudioFile from AudioFileScanner
setDataFromProbe(fileData, probeData) {
this.index = fileData.index || null
this.ino = fileData.ino || null
this.filename = fileData.filename
this.ext = fileData.ext
this.path = fileData.path
this.fullPath = fileData.fullPath
this.mtimeMs = fileData.mtimeMs || 0
this.ctimeMs = fileData.ctimeMs || 0
this.birthtimeMs = fileData.birthtimeMs || 0
this.addedAt = Date.now()
this.trackNumFromMeta = fileData.trackNumFromMeta
this.discNumFromMeta = fileData.discNumFromMeta
this.trackNumFromFilename = fileData.trackNumFromFilename
this.discNumFromFilename = fileData.discNumFromFilename
this.format = probeData.format
this.duration = probeData.duration
this.size = probeData.size
this.bitRate = probeData.bitRate || null
this.language = probeData.language
this.codec = probeData.codec || null
this.timeBase = probeData.timeBase
this.channels = probeData.channels
this.channelLayout = probeData.channelLayout
this.chapters = probeData.chapters || []
this.metadata = probeData.audioFileMetadata
this.embeddedCoverArt = probeData.embeddedCoverArt
}
validateTrackIndex() {
var numFromMeta = isNullOrNaN(this.trackNumFromMeta) ? null : Number(this.trackNumFromMeta)
var numFromFilename = isNullOrNaN(this.trackNumFromFilename) ? null : Number(this.trackNumFromFilename)
if (numFromMeta !== null) return numFromMeta
if (numFromFilename !== null) return numFromFilename
this.invalid = true
this.error = 'Failed to get track number'
return null
}
setDuplicateTrackNumber(num) {
this.invalid = true
this.error = 'Duplicate track number "' + num + '"'
}
syncChapters(updatedChapters) {
if (this.chapters.length !== updatedChapters.length) {
this.chapters = updatedChapters.map(ch => ({ ...ch }))
return true
} else if (updatedChapters.length === 0) {
if (this.chapters.length > 0) {
this.chapters = []
return true
}
return false
}
var hasUpdates = false
for (let i = 0; i < updatedChapters.length; i++) {
if (JSON.stringify(updatedChapters[i]) !== JSON.stringify(this.chapters[i])) {
hasUpdates = true
}
}
if (hasUpdates) {
this.chapters = updatedChapters.map(ch => ({ ...ch }))
}
return hasUpdates
}
clone() {
return new AudioFile(this.toJSON())
}
// If the file or parent directory was renamed it is synced here
syncFile(newFile) {
var hasUpdates = false
var keysToSync = ['path', 'fullPath', 'ext', 'filename']
keysToSync.forEach((key) => {
if (newFile[key] !== undefined && newFile[key] !== this[key]) {
hasUpdates = true
this[key] = newFile[key]
}
})
return hasUpdates
}
updateFromScan(scannedAudioFile) {
var hasUpdated = false
var newjson = scannedAudioFile.toJSON()
if (this.manuallyVerified) newjson.manuallyVerified = true
if (this.exclude) newjson.exclude = true
newjson.addedAt = this.addedAt
for (const key in newjson) {
if (key === 'metadata') {
if (!this.metadata || !this.metadata.isEqual(scannedAudioFile.metadata)) {
this.metadata = scannedAudioFile.metadata
hasUpdated = true
}
} else if (key === 'chapters') {
if (this.syncChapters(newjson.chapters || [])) {
hasUpdated = true
}
} else if (this[key] !== newjson[key]) {
// console.log(this.filename, 'key', key, 'updated', this[key], newjson[key])
this[key] = newjson[key]
hasUpdated = true
}
}
return hasUpdated
}
}
module.exports = AudioFile

View File

@ -1,116 +0,0 @@
var { bytesPretty } = require('../../utils/fileUtils')
class AudioTrack {
constructor(audioTrack = null) {
this.index = null
this.ino = null
this.path = null
this.fullPath = null
this.ext = null
this.filename = null
this.format = null
this.duration = null
this.size = null
this.bitRate = null
this.language = null
this.codec = null
this.timeBase = null
this.channels = null
this.channelLayout = null
if (audioTrack) {
this.construct(audioTrack)
}
}
construct(audioTrack) {
this.index = audioTrack.index
this.ino = audioTrack.ino || null
this.path = audioTrack.path
this.fullPath = audioTrack.fullPath
this.ext = audioTrack.ext
this.filename = audioTrack.filename
this.format = audioTrack.format
this.duration = audioTrack.duration
this.size = audioTrack.size
this.bitRate = audioTrack.bitRate
this.language = audioTrack.language
this.codec = audioTrack.codec
this.timeBase = audioTrack.timeBase
this.channels = audioTrack.channels
this.channelLayout = audioTrack.channelLayout
}
get name() {
return `${String(this.index).padStart(3, '0')}: ${this.filename} (${bytesPretty(this.size)}) [${this.duration}]`
}
toJSON() {
return {
index: this.index,
ino: this.ino,
path: this.path,
fullPath: this.fullPath,
ext: this.ext,
filename: this.filename,
format: this.format,
duration: this.duration,
size: this.size,
bitRate: this.bitRate,
language: this.language,
codec: this.codec,
timeBase: this.timeBase,
channels: this.channels,
channelLayout: this.channelLayout,
}
}
setData(probeData) {
this.index = probeData.index
this.ino = probeData.ino || null
this.path = probeData.path
this.fullPath = probeData.fullPath
this.ext = probeData.ext
this.filename = probeData.filename
this.format = probeData.format
this.duration = probeData.duration
this.size = probeData.size
this.bitRate = probeData.bitRate
this.language = probeData.language
this.codec = probeData.codec || null
this.timeBase = probeData.timeBase
this.channels = probeData.channels
this.channelLayout = probeData.channelLayout
}
syncMetadata(audioFile) {
var hasUpdates = false
var keysToSync = ['format', 'duration', 'size', 'bitRate', 'language', 'codec', 'timeBase', 'channels', 'channelLayout']
keysToSync.forEach((key) => {
if (audioFile[key] !== undefined && audioFile[key] !== this[key]) {
hasUpdates = true
this[key] = audioFile[key]
}
})
return hasUpdates
}
syncFile(newFile) {
var hasUpdates = false
var keysToSync = ['path', 'fullPath', 'ext', 'filename']
keysToSync.forEach((key) => {
if (newFile[key] !== undefined && newFile[key] !== this[key]) {
hasUpdates = true
this[key] = newFile[key]
}
})
return hasUpdates
}
}
module.exports = AudioTrack

File diff suppressed because it is too large Load Diff

View File

@ -1,69 +0,0 @@
class AudiobookFile {
constructor(data) {
this.ino = null
this.filetype = null
this.filename = null
this.ext = null
this.path = null
this.fullPath = null
this.size = null
this.mtimeMs = null
this.ctimeMs = null
this.birthtimeMs = null
this.addedAt = null
if (data) {
this.construct(data)
}
}
get isOPFFile() {
return this.ext ? this.ext.toLowerCase() === '.opf' : false
}
toJSON() {
return {
ino: this.ino || null,
filetype: this.filetype,
filename: this.filename,
ext: this.ext,
path: this.path,
fullPath: this.fullPath,
size: this.size,
mtimeMs: this.mtimeMs,
ctimeMs: this.ctimeMs,
birthtimeMs: this.birthtimeMs,
addedAt: this.addedAt
}
}
construct(data) {
this.ino = data.ino || null
this.filetype = data.filetype
this.filename = data.filename
this.ext = data.ext
this.path = data.path
this.fullPath = data.fullPath
this.size = data.size || 0
this.mtimeMs = data.mtimeMs || 0
this.ctimeMs = data.ctimeMs || 0
this.birthtimeMs = data.birthtimeMs || 0
this.addedAt = data.addedAt
}
setData(data) {
this.ino = data.ino || null
this.filetype = data.filetype
this.filename = data.filename
this.ext = data.ext
this.path = data.path
this.fullPath = data.fullPath
this.size = data.size || 0
this.mtimeMs = data.mtimeMs || 0
this.ctimeMs = data.ctimeMs || 0
this.birthtimeMs = data.birthtimeMs || 0
this.addedAt = Date.now()
}
}
module.exports = AudiobookFile

View File

@ -1,72 +0,0 @@
const { getId } = require('../../utils/index')
const Logger = require('../../Logger')
class Author {
constructor(author = null) {
this.id = null
this.name = null
this.description = null
this.asin = null
this.image = null
this.imageFullPath = null
this.createdAt = null
this.lastUpdate = null
if (author) {
this.construct(author)
}
}
construct(author) {
this.id = author.id
this.name = author.name
this.description = author.description
this.asin = author.asin
this.image = author.image
this.imageFullPath = author.imageFullPath
this.createdAt = author.createdAt
this.lastUpdate = author.lastUpdate
}
toJSON() {
return {
id: this.id,
name: this.name,
description: this.description,
asin: this.asin,
image: this.image,
imageFullPath: this.imageFullPath,
createdAt: this.createdAt,
lastUpdate: this.lastUpdate
}
}
setData(data) {
this.id = data.id ? data.id : getId('per')
this.name = data.name
this.description = data.description
this.asin = data.asin || null
this.image = data.image || null
this.imageFullPath = data.imageFullPath || null
this.createdAt = Date.now()
this.lastUpdate = Date.now()
}
update(payload) {
var hasUpdates = false
for (const key in payload) {
if (this[key] === undefined) continue;
if (this[key] !== payload[key]) {
hasUpdates = true
this[key] = payload[key]
}
}
if (hasUpdates) {
this.lastUpdate = Date.now()
}
return hasUpdates
}
}
module.exports = Author

View File

@ -1,417 +0,0 @@
const Path = require('path')
const Logger = require('../../Logger')
const parseAuthors = require('../../utils/parseNameString')
class Book {
constructor(book = null) {
this.title = null
this.subtitle = null
this.author = null
this.authorFL = null
this.authorLF = null
this.authors = []
this.narrator = null
this.narratorFL = null
this.series = null
this.volumeNumber = null
this.publishYear = null
this.publisher = null
this.description = null
this.isbn = null
this.asin = null
this.language = null
this.cover = null
this.coverFullPath = null
this.genres = []
this.lastUpdate = null
// Should not continue looking up a cover when it is not findable
this.lastCoverSearch = null
this.lastCoverSearchTitle = null
this.lastCoverSearchAuthor = null
if (book) {
this.construct(book)
}
}
get _title() { return this.title || '' }
get _subtitle() { return this.subtitle || '' }
get _narrator() { return this.narrator || '' }
get _author() { return this.authorFL || '' }
get _series() { return this.series || '' }
get _authorsList() { return this._author.split(', ') }
get _narratorsList() { return this._narrator.split(', ') }
get _genres() { return this.genres || [] }
get _language() { return this.language || '' }
get _isbn() { return this.isbn || '' }
get _asin() { return this.asin || '' }
get genresCommaSeparated() { return this._genres.join(', ') }
get titleIgnorePrefix() {
if (this._title.toLowerCase().startsWith('the ')) {
return this._title.substr(4) + ', The'
}
return this._title
}
get seriesIgnorePrefix() {
if (this._series.toLowerCase().startsWith('the ')) {
return this._series.substr(4) + ', The'
}
return this._series
}
get shouldSearchForCover() {
if (this.cover) return false
if (this.authorFL !== this.lastCoverSearchAuthor || this.title !== this.lastCoverSearchTitle || !this.lastCoverSearch) return true
var timeSinceLastSearch = Date.now() - this.lastCoverSearch
return timeSinceLastSearch > 1000 * 60 * 60 * 24 * 7 // every 7 days do another lookup
}
construct(book) {
this.title = book.title
this.subtitle = book.subtitle || null
this.author = book.author
this.authors = (book.authors || []).map(a => ({ ...a }))
this.authorFL = book.authorFL || null
this.authorLF = book.authorLF || null
this.narrator = book.narrator || book.narrarator || null // Mispelled initially... need to catch those
this.narratorFL = book.narratorFL || null
this.series = book.series
this.volumeNumber = book.volumeNumber || null
this.publishYear = book.publishYear
this.publisher = book.publisher
this.description = book.description
this.isbn = book.isbn || null
this.asin = book.asin || null
this.language = book.language || null
this.cover = book.cover
this.coverFullPath = book.coverFullPath || null
this.genres = book.genres
this.lastUpdate = book.lastUpdate || Date.now()
this.lastCoverSearch = book.lastCoverSearch || null
this.lastCoverSearchTitle = book.lastCoverSearchTitle || null
this.lastCoverSearchAuthor = book.lastCoverSearchAuthor || null
// narratorFL added in v1.6.21 to support multi-narrators
if (this.narrator && !this.narratorFL) {
this.setParseNarrator(this.narrator)
}
}
toJSON() {
return {
title: this.title,
subtitle: this.subtitle,
author: this.author,
authors: this.authors,
authorFL: this.authorFL,
authorLF: this.authorLF,
narrator: this.narrator,
narratorFL: this.narratorFL,
series: this.series,
volumeNumber: this.volumeNumber,
publishYear: this.publishYear,
publisher: this.publisher,
description: this.description,
isbn: this.isbn,
asin: this.asin,
language: this.language,
cover: this.cover,
coverFullPath: this.coverFullPath,
genres: this.genres,
lastUpdate: this.lastUpdate,
lastCoverSearch: this.lastCoverSearch,
lastCoverSearchTitle: this.lastCoverSearchTitle,
lastCoverSearchAuthor: this.lastCoverSearchAuthor
}
}
setParseAuthor(author) {
if (!author) {
var hasUpdated = this.authorFL || this.authorLF
this.authorFL = null
this.authorLF = null
return hasUpdated
}
try {
var { authorLF, authorFL } = parseAuthors.parse(author)
var hasUpdated = authorLF !== this.authorLF || authorFL !== this.authorFL
this.authorFL = authorFL || null
this.authorLF = authorLF || null
return hasUpdated
} catch (err) {
Logger.error('[Book] Parse authors failed', err)
return false
}
}
setParseNarrator(narrator) {
if (!narrator) {
var hasUpdated = this.narratorFL
this.narratorFL = null
return hasUpdated
}
try {
var { authorFL } = parseAuthors.parse(narrator)
var hasUpdated = authorFL !== this.narratorFL
this.narratorFL = authorFL || null
return hasUpdated
} catch (err) {
Logger.error('[Book] Parse narrator failed', err)
return false
}
}
setData(data) {
this.title = data.title || null
this.subtitle = data.subtitle || null
this.author = data.author || null
this.authors = data.authors || []
this.narrator = data.narrator || data.narrarator || null
this.series = data.series || null
this.volumeNumber = data.volumeNumber || null
this.publishYear = data.publishYear || null
this.description = data.description || null
this.isbn = data.isbn || null
this.asin = data.asin || null
this.language = data.language || null
this.cover = data.cover || null
this.coverFullPath = data.coverFullPath || null
this.genres = data.genres || []
this.lastUpdate = Date.now()
this.lastCoverSearch = data.lastCoverSearch || null
this.lastCoverSearchTitle = data.lastCoverSearchTitle || null
this.lastCoverSearchAuthor = data.lastCoverSearchAuthor || null
if (data.author) {
this.setParseAuthor(this.author)
}
if (data.narrator) {
this.setParseNarrator(this.narrator)
}
}
update(payload) {
var hasUpdates = false
// Clean cover paths if passed
if (payload.cover) {
if (!payload.cover.startsWith('http:') && !payload.cover.startsWith('https:')) {
payload.cover = payload.cover.replace(/\\/g, '/')
if (payload.coverFullPath) payload.coverFullPath = payload.coverFullPath.replace(/\\/g, '/')
else {
Logger.warn(`[Book] "${this.title}" updating book cover to "${payload.cover}" but no full path was passed`)
}
}
} else if (payload.coverFullPath) {
Logger.warn(`[Book] "${this.title}" updating book full cover path to "${payload.coverFullPath}" but no relative path was passed`)
payload.coverFullPath = payload.coverFullPath.replace(/\\/g, '/')
}
for (const key in payload) {
if (payload[key] === undefined) continue;
if (key === 'genres') {
if (payload['genres'] === null && this.genres !== null) {
this.genres = []
hasUpdates = true
} else if (payload['genres'].join(',') !== this.genres.join(',')) {
this.genres = payload['genres']
hasUpdates = true
}
} else if (key === 'author') {
if (this.author !== payload.author) {
this.author = payload.author || null
hasUpdates = true
}
if (this.setParseAuthor(this.author)) {
hasUpdates = true
}
} else if (key === 'narrator') {
if (this.narrator !== payload.narrator) {
this.narrator = payload.narrator || null
hasUpdates = true
}
if (this.setParseNarrator(this.narrator)) {
hasUpdates = true
}
} else if (this[key] !== undefined && payload[key] !== this[key]) {
this[key] = payload[key]
hasUpdates = true
}
}
if (hasUpdates) {
this.lastUpdate = Date.now()
}
return hasUpdates
}
updateLastCoverSearch(coverWasFound) {
this.lastCoverSearch = coverWasFound ? null : Date.now()
this.lastCoverSearchAuthor = coverWasFound ? null : this.authorFL
this.lastCoverSearchTitle = coverWasFound ? null : this.title
}
updateCover(cover, coverFullPath) {
if (!cover) return false
if (!cover.startsWith('http:') && !cover.startsWith('https:')) {
cover = cover.replace(/\\/g, '/')
this.coverFullPath = coverFullPath.replace(/\\/g, '/')
} else {
this.coverFullPath = cover
}
this.cover = cover
this.lastUpdate = Date.now()
return true
}
removeCover() {
this.cover = null
this.coverFullPath = null
this.lastUpdate = Date.now()
}
// If audiobook directory path was changed, check and update properties set from dirnames
// May be worthwhile checking if these were manually updated and not override manual updates
syncPathsUpdated(audiobookData) {
var keysToSync = ['author', 'title', 'series', 'publishYear', 'volumeNumber']
var syncPayload = {}
keysToSync.forEach((key) => {
if (audiobookData[key]) syncPayload[key] = audiobookData[key]
})
if (!Object.keys(syncPayload).length) return false
return this.update(syncPayload)
}
isSearchMatch(search) {
return this._title.toLowerCase().includes(search) || this._subtitle.toLowerCase().includes(search) || this._author.toLowerCase().includes(search) || this._series.toLowerCase().includes(search)
}
getQueryMatches(search) {
var titleMatch = this._title.toLowerCase().includes(search)
var subtitleMatch = this._subtitle.toLowerCase().includes(search)
var authorsMatched = this._authorsList.filter(auth => auth.toLowerCase().includes(search))
// var authorMatch = this._author.toLowerCase().includes(search)
var seriesMatch = this._series.toLowerCase().includes(search)
// ISBN match has to be exact to prevent isbn matches to flood results. Remove dashes since isbn might have those
var isbnMatch = this._isbn.toLowerCase().replace(/-/g, '') === search.replace(/-/g, '')
var asinMatch = this._asin.toLowerCase() === search
var bookMatchKey = titleMatch ? 'title' : subtitleMatch ? 'subtitle' : authorsMatched.length ? 'authorFL' : seriesMatch ? 'series' : isbnMatch ? 'isbn' : asinMatch ? 'asin' : false
var bookMatchText = bookMatchKey ? this[bookMatchKey] : ''
return {
book: bookMatchKey,
bookMatchText,
authors: authorsMatched.length ? authorsMatched : false,
series: seriesMatch ? this._series : false
}
}
parseGenresTag(genreTag) {
if (!genreTag || !genreTag.length) return []
var separators = ['/', '//', ';']
for (let i = 0; i < separators.length; i++) {
if (genreTag.includes(separators[i])) {
return genreTag.split(separators[i]).map(genre => genre.trim()).filter(g => !!g)
}
}
return [genreTag]
}
setDetailsFromFileMetadata(audioFileMetadata, overrideExistingDetails = false) {
const MetadataMapArray = [
{
tag: 'tagComposer',
key: 'narrator'
},
{
tag: 'tagDescription',
key: 'description'
},
{
tag: 'tagPublisher',
key: 'publisher'
},
{
tag: 'tagDate',
key: 'publishYear'
},
{
tag: 'tagSubtitle',
key: 'subtitle'
},
{
tag: 'tagAlbum',
altTag: 'tagTitle',
key: 'title',
},
{
tag: 'tagArtist',
altTag: 'tagAlbumArtist',
key: 'author'
},
{
tag: 'tagGenre',
key: 'genres'
},
{
tag: 'tagSeries',
key: 'series'
},
{
tag: 'tagSeriesPart',
key: 'volumeNumber'
},
{
tag: 'tagIsbn',
key: 'isbn'
},
{
tag: 'tagLanguage',
key: 'language'
},
{
tag: 'tagASIN',
key: 'asin'
}
]
var updatePayload = {}
// Metadata is only mapped to the book if it is empty
MetadataMapArray.forEach((mapping) => {
var value = audioFileMetadata[mapping.tag]
var tagToUse = mapping.tag
if (!value && mapping.altTag) {
value = audioFileMetadata[mapping.altTag]
tagToUse = mapping.altTag
}
if (value) {
// Genres can contain multiple
if (mapping.key === 'genres' && (!this[mapping.key].length || !this[mapping.key] || overrideExistingDetails)) {
updatePayload[mapping.key] = this.parseGenresTag(audioFileMetadata[tagToUse])
// Logger.debug(`[Book] Mapping metadata to key ${tagToUse} => ${mapping.key}: ${updatePayload[mapping.key].join(',')}`)
} else if (!this[mapping.key] || overrideExistingDetails) {
updatePayload[mapping.key] = audioFileMetadata[tagToUse]
// Logger.debug(`[Book] Mapping metadata to key ${tagToUse} => ${mapping.key}: ${updatePayload[mapping.key]}`)
}
}
})
if (Object.keys(updatePayload).length) {
return this.update(updatePayload)
}
return false
}
}
module.exports = Book

View File

@ -1,231 +0,0 @@
const Stream = require('../Stream')
// const StreamTest = require('./test/StreamTest')
const Logger = require('../../Logger')
const fs = require('fs-extra')
const Path = require('path')
class StreamManager {
constructor(db, emitter, clientEmitter) {
this.db = db
this.emitter = emitter
this.clientEmitter = clientEmitter
this.streams = []
this.StreamsPath = Path.join(global.MetadataPath, 'streams')
}
getStream(streamId) {
return this.streams.find(s => s.id === streamId)
}
removeStream(stream) {
this.streams = this.streams.filter(s => s.id !== stream.id)
}
async openStream(client, libraryItem, transcodeOptions = {}) {
if (!client || !client.user) {
Logger.error('[StreamManager] Cannot open stream invalid client', client)
return
}
var stream = new Stream(this.StreamsPath, client, libraryItem, transcodeOptions)
stream.on('closed', () => {
this.removeStream(stream)
})
this.streams.push(stream)
await stream.generatePlaylist()
stream.start()
Logger.info('Stream Opened for client', client.user.username, 'for item', stream.itemTitle, 'with streamId', stream.id)
client.stream = stream
client.user.stream = stream.id
return stream
}
ensureStreamsDir() {
return fs.ensureDir(this.StreamsPath)
}
removeOrphanStreamFiles(streamId) {
try {
var StreamsPath = Path.join(this.StreamsPath, streamId)
return fs.remove(StreamsPath)
} catch (error) {
Logger.debug('No orphan stream', streamId)
return false
}
}
async removeOrphanStreams() {
try {
var dirs = await fs.readdir(this.StreamsPath)
if (!dirs || !dirs.length) return true
await Promise.all(dirs.map(async (dirname) => {
var fullPath = Path.join(this.StreamsPath, dirname)
Logger.info(`Removing Orphan Stream ${dirname}`)
return fs.remove(fullPath)
}))
return true
} catch (error) {
Logger.debug('No orphan stream', error)
return false
}
}
async openStreamApiRequest(res, user, libraryItem) {
Logger.info(`[StreamManager] User "${user.username}" open stream request for "${libraryItem.media.metadata.title}"`)
var client = {
user
}
var stream = await this.openStream(client, libraryItem)
this.db.updateUserStream(client.user.id, stream.id)
res.json({
libraryItemId: libraryItem.id,
startTime: stream.startTime,
streamId: stream.id,
streamUrl: stream.clientPlaylistUri
})
}
async openStreamSocketRequest(socket, libraryItemId) {
Logger.info('[StreamManager] Open Stream Request', socket.id, libraryItemId)
var libraryItem = this.db.libraryItems.find(li => li.id === libraryItemId)
var client = socket.sheepClient
if (client.stream) {
Logger.info('Closing client stream first', client.stream.id)
await client.stream.close()
client.user.stream = null
client.stream = null
}
var stream = await this.openStream(client, libraryItem)
this.db.updateUserStream(client.user.id, stream.id)
}
async closeStreamRequest(socket) {
Logger.info('Close Stream Request', socket.id)
var client = socket.sheepClient
if (!client || !client.stream) {
Logger.error('No stream for client', (client && client.user) ? client.user.username : 'No Client')
client.socket.emit('stream_closed', 'n/a')
return
}
// var streamId = client.stream.id
await client.stream.close()
client.user.stream = null
client.stream = null
this.db.updateUserStream(client.user.id, null)
}
async closeStreamApiRequest(userId, streamId) {
Logger.info('[StreamManager] Close Stream Api Request', streamId)
var stream = this.streams.find(s => s.id === streamId)
if (!stream) {
Logger.warn('[StreamManager] Stream not found', streamId)
return
}
if (!stream.client || !stream.client.user || stream.client.user.id !== userId) {
Logger.warn(`[StreamManager] Stream close request from invalid user ${userId}`, stream.client)
return
}
stream.client.user.stream = null
stream.client.stream = null
this.db.updateUserStream(stream.client.user.id, null)
await stream.close()
this.streams = this.streams.filter(s => s.id !== streamId)
Logger.info(`[StreamManager] Stream ${streamId} closed via API request by ${userId}`)
}
streamSync(socket, syncData) {
const client = socket.sheepClient
if (!client || !client.stream) {
Logger.error('[StreamManager] streamSync: No stream for client', (client && client.user) ? client.user.id : 'No Client')
return
}
if (client.stream.id !== syncData.streamId) {
Logger.error('[StreamManager] streamSync: Stream id mismatch on stream update', syncData.streamId, client.stream.id)
return
}
if (!client.user) {
Logger.error('[StreamManager] streamSync: No User for client', client)
return
}
// const { timeListened, currentTime, streamId } = syncData
var listeningSession = client.stream.syncStream(syncData)
if (listeningSession && listeningSession.timeListening > 0) {
// Save listening session
var existingListeningSession = this.db.sessions.find(s => s.id === listeningSession.id)
if (existingListeningSession) {
this.db.updateEntity('session', listeningSession)
} else {
this.db.sessions.push(listeningSession.toJSON()) // Insert right away to prevent duplicate session
this.db.insertEntity('session', listeningSession)
}
}
var userAudiobook = client.user.updateAudiobookProgressFromStream(client.stream)
this.db.updateEntity('user', client.user)
if (userAudiobook) {
this.clientEmitter(client.user.id, 'current_user_audiobook_update', {
id: userAudiobook.audiobookId,
data: userAudiobook.toJSON()
})
}
}
streamSyncFromApi(req, res) {
var user = req.user
var syncData = req.body
var stream = this.streams.find(s => s.id === syncData.streamId)
if (!stream) {
Logger.error(`[StreamManager] streamSyncFromApi stream not found ${syncData.streamId}`)
return res.status(404).send('Stream not found')
}
if (stream.userToken !== user.token) {
Logger.error(`[StreamManager] streamSyncFromApi Invalid stream not owned by user`)
return res.status(500).send('Invalid stream auth')
}
var listeningSession = stream.syncStream(syncData)
if (listeningSession && listeningSession.timeListening > 0) {
// Save listening session
var existingListeningSession = this.db.sessions.find(s => s.id === listeningSession.id)
if (existingListeningSession) {
this.db.updateEntity('session', listeningSession)
} else {
this.db.sessions.push(listeningSession.toJSON()) // Insert right away to prevent duplicate session
this.db.insertEntity('session', listeningSession)
}
}
var userAudiobook = user.updateAudiobookProgressFromStream(stream)
this.db.updateEntity('user', user)
if (userAudiobook) {
this.clientEmitter(user.id, 'current_user_audiobook_update', {
id: userAudiobook.audiobookId,
data: userAudiobook.toJSON()
})
}
res.sendStatus(200)
}
}
module.exports = StreamManager

View File

@ -1,136 +0,0 @@
const Logger = require('../../Logger')
const AudioBookmark = require('../user/AudioBookmark')
class UserAudiobookData {
constructor(progress) {
this.audiobookId = null
this.totalDuration = null // seconds
this.progress = null // 0 to 1
this.currentTime = null // seconds
this.isRead = false
this.lastUpdate = null
this.startedAt = null
this.finishedAt = null
this.bookmarks = []
if (progress) {
this.construct(progress)
}
}
bookmarksToJSON() {
if (!this.bookmarks) return []
return this.bookmarks.filter((b) => {
if (!b.toJSON) {
Logger.error(`[UserAudiobookData] Invalid bookmark ${JSON.stringify(b)}`)
return false
}
return true
}).map(b => b.toJSON())
}
toJSON() {
return {
audiobookId: this.audiobookId,
totalDuration: this.totalDuration,
progress: this.progress,
currentTime: this.currentTime,
isRead: this.isRead,
lastUpdate: this.lastUpdate,
startedAt: this.startedAt,
finishedAt: this.finishedAt,
bookmarks: this.bookmarksToJSON()
}
}
construct(progress) {
this.audiobookId = progress.audiobookId
this.totalDuration = progress.totalDuration
this.progress = progress.progress
this.currentTime = progress.currentTime
this.isRead = !!progress.isRead
this.lastUpdate = progress.lastUpdate
this.startedAt = progress.startedAt
this.finishedAt = progress.finishedAt || null
if (progress.bookmarks) {
this.bookmarks = progress.bookmarks.map(b => new AudioBookmark(b))
} else {
this.bookmarks = []
}
}
updateProgressFromStream(stream) {
this.audiobookId = stream.libraryItemId
this.totalDuration = stream.totalDuration
this.progress = stream.clientProgress
this.currentTime = stream.clientCurrentTime
this.lastUpdate = Date.now()
if (!this.startedAt) {
this.startedAt = Date.now()
}
// If has < 10 seconds remaining mark as read
var timeRemaining = this.totalDuration - this.currentTime
if (timeRemaining < 10) {
this.isRead = true
this.progress = 1
this.finishedAt = Date.now()
} else {
this.isRead = false
this.finishedAt = null
}
}
update(payload) {
var hasUpdates = false
for (const key in payload) {
if (this[key] !== undefined && payload[key] !== this[key]) {
if (key === 'isRead') {
if (!payload[key]) { // Updating to Not Read - Reset progress and current time
this.finishedAt = null
this.progress = 0
this.currentTime = 0
} else { // Updating to Read
if (!this.finishedAt) this.finishedAt = Date.now()
this.progress = 1
}
}
this[key] = payload[key]
hasUpdates = true
}
}
if (!this.startedAt) {
this.startedAt = Date.now()
}
if (hasUpdates) {
this.lastUpdate = Date.now()
}
return hasUpdates
}
checkBookmarkExists(time) {
return this.bookmarks.find(bm => bm.time === time)
}
createBookmark(time, title) {
var newBookmark = new AudioBookmark()
newBookmark.setData(time, title)
this.bookmarks.push(newBookmark)
return newBookmark
}
updateBookmark(time, title) {
var bookmark = this.bookmarks.find(bm => bm.time === time)
if (!bookmark) return false
bookmark.title = title
return bookmark
}
deleteBookmark(time) {
this.bookmarks = this.bookmarks.filter(bm => bm.time !== time)
}
}
module.exports = UserAudiobookData

View File

@ -1,98 +0,0 @@
const Logger = require('../../Logger')
const date = require('date-and-time')
const { getId } = require('../../utils/index')
class UserListeningSession {
constructor(session) {
this.id = null
this.sessionType = 'listeningSession'
this.userId = null
this.audiobookId = null
this.audiobookTitle = null
this.audiobookAuthor = null
this.audiobookDuration = 0
this.audiobookGenres = []
this.date = null
this.dayOfWeek = null
this.timeListening = null
this.lastUpdate = null
this.startedAt = null
if (session) {
this.construct(session)
}
}
toJSON() {
return {
id: this.id,
sessionType: this.sessionType,
userId: this.userId,
audiobookId: this.audiobookId,
audiobookTitle: this.audiobookTitle,
audiobookAuthor: this.audiobookAuthor,
audiobookDuration: this.audiobookDuration,
audiobookGenres: [...this.audiobookGenres],
date: this.date,
dayOfWeek: this.dayOfWeek,
timeListening: this.timeListening,
lastUpdate: this.lastUpdate,
startedAt: this.startedAt
}
}
construct(session) {
this.id = session.id
this.sessionType = session.sessionType
this.userId = session.userId
this.audiobookId = session.audiobookId
this.audiobookTitle = session.audiobookTitle
this.audiobookAuthor = session.audiobookAuthor
this.audiobookDuration = session.audiobookDuration || 0
this.audiobookGenres = session.audiobookGenres
this.date = session.date
this.dayOfWeek = session.dayOfWeek
this.timeListening = session.timeListening || null
this.lastUpdate = session.lastUpdate || null
this.startedAt = session.startedAt
}
setData(libraryItem, user) {
this.id = getId('ls')
this.userId = user.id
this.audiobookId = libraryItem.id
// TODO: For podcasts this needs to be generic
this.audiobookTitle = libraryItem.media.metadata.title || ''
this.audiobookAuthor = libraryItem.media.metadata.authorName || ''
this.audiobookDuration = libraryItem.media.duration || 0
this.audiobookGenres = [...libraryItem.media.metadata.genres]
this.timeListening = 0
this.lastUpdate = Date.now()
this.startedAt = Date.now()
}
addListeningTime(timeListened) {
if (timeListened && !isNaN(timeListened)) {
if (!this.date) {
// Set date info on first listening update
this.date = date.format(new Date(), 'YYYY-MM-DD')
this.dayOfWeek = date.format(new Date(), 'dddd')
}
this.timeListening += timeListened
this.lastUpdate = Date.now()
}
}
// New date since start of listening session
checkDateRollover() {
if (!this.date) return false
return date.format(new Date(), 'YYYY-MM-DD') !== this.date
}
}
module.exports = UserListeningSession

View File

@ -7,9 +7,6 @@ const { PlayMethod } = require('./constants')
const { getId } = require('./index')
const Logger = require('../Logger')
const LegacyAudiobook = require('../objects/legacy/Audiobook')
const UserAudiobookData = require('../objects/legacy/UserAudiobookData')
const Library = require('../objects/Library')
const LibraryItem = require('../objects/LibraryItem')
const Book = require('../objects/mediaTypes/Book')
@ -40,14 +37,17 @@ var existingDbSeries = []
async function loadAudiobooks() {
var audiobookPath = Path.join(global.ConfigPath, 'audiobooks')
Logger.debug(`[dbMigration] loadAudiobooks path ${audiobookPath}`)
var pathExists = await fs.pathExists(audiobookPath)
if (!pathExists) {
Logger.debug(`[dbMigration] loadAudiobooks path does not exist ${audiobookPath}`)
return []
}
var audiobooksDb = new njodb.Database(audiobookPath)
return audiobooksDb.select(() => true).then((results) => {
return results.data.map(a => new LegacyAudiobook(a))
Logger.debug(`[dbMigration] loadAudiobooks select results ${results.data.length}`)
return results.data
})
}
@ -65,7 +65,7 @@ function makeAuthorsFromOldAb(authorsList) {
var newAuthor = new Author()
newAuthor.setData({ name: authorName })
authorsToAdd.push(newAuthor)
Logger.debug(`>>> Created new author named "${authorName}"`)
// Logger.debug(`>>> Created new author named "${authorName}"`)
return newAuthor.toJSONMinimal()
})
}
@ -96,7 +96,8 @@ function makeFilesFromOldAb(audiobook) {
var libraryFiles = []
var ebookFiles = []
var audioFiles = audiobook._audioFiles.map((af) => {
var _audioFiles = audiobook.audioFiles || []
var audioFiles = _audioFiles.map((af) => {
var fileMetadata = new FileMetadata(af)
fileMetadata.path = af.fullPath
fileMetadata.relPath = getRelativePath(af.fullPath, audiobook.fullPath)
@ -118,7 +119,8 @@ function makeFilesFromOldAb(audiobook) {
return newAudioFile
})
audiobook._otherFiles.forEach((file) => {
var _otherFiles = audiobook.otherFiles || []
_otherFiles.forEach((file) => {
var fileMetadata = new FileMetadata(file)
fileMetadata.path = file.fullPath
fileMetadata.relPath = getRelativePath(file.fullPath, audiobook.fullPath)
@ -182,10 +184,10 @@ function makeLibraryItemFromOldAb(audiobook) {
var bookMetadata = new BookMetadata(audiobook.book)
bookMetadata.publishedYear = audiobook.book.publishYear || null
if (audiobook.book.narrator) {
bookMetadata.narrators = audiobook.book._narratorsList
bookMetadata.narrators = (audiobook.book.narrator || '').split(', ')
}
// Returns array of json minimal authors
bookMetadata.authors = makeAuthorsFromOldAb(audiobook.book._authorsList)
bookMetadata.authors = makeAuthorsFromOldAb((audiobook.book.authorFL || '').split(', '))
// Returns array of json minimal series
if (audiobook.book.series) {
@ -276,7 +278,7 @@ function cleanUserObject(db, userObj) {
cleanedUserPayload.bookmarks = cleanedUserPayload.bookmarks.concat(cleanedBookmarks)
}
var userAudiobookData = new UserAudiobookData(userObj.audiobooks[audiobookId]) // Legacy object
var userAudiobookData = userObj.audiobooks[audiobookId]
var liProgress = new MediaProgress() // New Progress Object
liProgress.id = userAudiobookData.audiobookId
liProgress.libraryItemId = userAudiobookData.audiobookId