mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-30 09:48:52 +01:00
Removing remaining legacy objects, remove njodb error for fileExists
This commit is contained in:
parent
99e2ea228d
commit
c60807f998
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "1.7.2",
|
||||
"version": "1.7.3",
|
||||
"description": "Audiobook manager and player",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
@ -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": {
|
||||
|
@ -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()
|
||||
|
@ -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
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
@ -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
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user