Adding inode to files and audiobooks to support renaming, setting up watcher and removing chokidar

This commit is contained in:
advplyr 2021-08-25 17:36:54 -05:00
parent 0c1a29adbf
commit cb40e063da
17 changed files with 558 additions and 234 deletions

135
server/AudioFile.js Normal file
View file

@ -0,0 +1,135 @@
class AudioFile {
constructor(data) {
this.index = null
this.ino = null
this.filename = null
this.ext = null
this.path = null
this.fullPath = null
this.addedAt = 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.tagAlbum = null
this.tagArtist = null
this.tagGenre = null
this.tagTitle = null
this.tagTrack = null
this.manuallyVerified = false
this.invalid = 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,
addedAt: this.addedAt,
manuallyVerified: !!this.manuallyVerified,
invalid: !!this.invalid,
error: this.error || null,
format: this.format,
duration: this.duration,
size: this.size,
bitRate: this.bitRate,
language: this.language,
timeBase: this.timeBase,
channels: this.channels,
channelLayout: this.channelLayout,
tagAlbum: this.tagAlbum,
tagArtist: this.tagArtist,
tagGenre: this.tagGenre,
tagTitle: this.tagTitle,
tagTrack: this.tagTrack
}
}
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.addedAt = data.addedAt
this.manuallyVerified = !!data.manuallyVerified
this.invalid = !!data.invalid
this.error = data.error || null
this.format = data.format
this.duration = data.duration
this.size = data.size
this.bitRate = data.bitRate
this.language = data.language
this.codec = data.codec
this.timeBase = data.timeBase
this.channels = data.channels
this.channelLayout = data.channelLayout
this.tagAlbum = data.tagAlbum
this.tagArtist = data.tagArtist
this.tagGenre = data.tagGenre
this.tagTitle = data.tagTitle
this.tagTrack = data.tagTrack
}
setData(data) {
this.index = data.index || null
this.ino = data.ino
this.filename = data.filename
this.ext = data.ext
this.path = data.path
this.fullPath = data.fullPath
this.addedAt = Date.now()
this.manuallyVerified = !!data.manuallyVerified
this.invalid = !!data.invalid
this.error = data.error || null
this.format = data.format
this.duration = data.duration
this.size = data.size
this.bitRate = data.bit_rate
this.language = data.language
this.codec = data.codec
this.timeBase = data.time_base
this.channels = data.channels
this.channelLayout = data.channel_layout
this.tagAlbum = data.file_tag_album || null
this.tagArtist = data.file_tag_artist || null
this.tagGenre = data.file_tag_genre || null
this.tagTitle = data.file_tag_title || null
this.tagTrack = data.file_tag_track || null
}
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 = AudioFile

View file

@ -3,6 +3,8 @@ 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
@ -31,6 +33,8 @@ class AudioTrack {
construct(audioTrack) {
this.index = audioTrack.index
this.ino = audioTrack.ino || null
this.path = audioTrack.path
this.fullPath = audioTrack.fullPath
this.ext = audioTrack.ext
@ -45,6 +49,12 @@ class AudioTrack {
this.timeBase = audioTrack.timeBase
this.channels = audioTrack.channels
this.channelLayout = audioTrack.channelLayout
this.tagAlbum = audioTrack.tagAlbum
this.tagArtist = audioTrack.tagArtist
this.tagGenre = audioTrack.tagGenre
this.tagTitle = audioTrack.tagTitle
this.tagTrack = audioTrack.tagTrack
}
get name() {
@ -54,6 +64,7 @@ class AudioTrack {
toJSON() {
return {
index: this.index,
ino: this.ino,
path: this.path,
fullPath: this.fullPath,
ext: this.ext,
@ -65,12 +76,19 @@ class AudioTrack {
language: this.language,
timeBase: this.timeBase,
channels: this.channels,
channelLayout: this.channelLayout
channelLayout: this.channelLayout,
tagAlbum: this.tagAlbum,
tagArtist: this.tagArtist,
tagGenre: this.tagGenre,
tagTitle: this.tagTitle,
tagTrack: this.tagTrack
}
}
setData(probeData) {
this.index = probeData.index
this.ino = probeData.ino || null
this.path = probeData.path
this.fullPath = probeData.fullPath
this.ext = probeData.ext
@ -92,5 +110,17 @@ class AudioTrack {
this.tagTitle = probeData.file_tag_title || null
this.tagTrack = probeData.file_tag_track || null
}
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

View file

@ -1,15 +1,20 @@
const Path = require('path')
const { bytesPretty, elapsedPretty } = require('./utils/fileUtils')
const { comparePaths } = require('./utils/index')
const { comparePaths, getIno } = require('./utils/index')
const Logger = require('./Logger')
const Book = require('./Book')
const AudioTrack = require('./AudioTrack')
const AudioFile = require('./AudioFile')
const AudiobookFile = require('./AudiobookFile')
class Audiobook {
constructor(audiobook = null) {
this.id = null
this.ino = null // Inode
this.path = null
this.fullPath = null
this.addedAt = null
this.lastUpdate = null
@ -30,19 +35,18 @@ class Audiobook {
construct(audiobook) {
this.id = audiobook.id
this.ino = audiobook.ino || null
this.path = audiobook.path
this.fullPath = audiobook.fullPath
this.addedAt = audiobook.addedAt
this.lastUpdate = audiobook.lastUpdate || this.addedAt
this.tracks = audiobook.tracks.map(track => {
return new AudioTrack(track)
})
this.tracks = audiobook.tracks.map(track => new AudioTrack(track))
this.missingParts = audiobook.missingParts
this.invalidParts = audiobook.invalidParts
this.audioFiles = audiobook.audioFiles
this.otherFiles = audiobook.otherFiles
this.audioFiles = audiobook.audioFiles.map(file => new AudioFile(file))
this.otherFiles = audiobook.otherFiles.map(file => new AudiobookFile(file))
this.tags = audiobook.tags
if (audiobook.book) {
@ -102,6 +106,7 @@ class Audiobook {
toJSON() {
return {
id: this.id,
ino: this.ino,
title: this.title,
author: this.author,
cover: this.cover,
@ -114,14 +119,15 @@ class Audiobook {
tags: this.tags,
book: this.bookToJSON(),
tracks: this.tracksToJSON(),
audioFiles: this.audioFiles,
otherFiles: this.otherFiles
audioFiles: (this.audioFiles || []).map(audioFile => audioFile.toJSON()),
otherFiles: (this.otherFiles || []).map(otherFile => otherFile.toJSON())
}
}
toJSONMinified() {
return {
id: this.id,
ino: this.ino,
book: this.bookToJSON(),
tags: this.tags,
path: this.path,
@ -140,9 +146,6 @@ class Audiobook {
toJSONExpanded() {
return {
id: this.id,
// title: this.title,
// author: this.author,
// cover: this.cover,
path: this.path,
fullPath: this.fullPath,
addedAt: this.addedAt,
@ -153,8 +156,8 @@ class Audiobook {
sizePretty: this.sizePretty,
missingParts: this.missingParts,
invalidParts: this.invalidParts,
audioFiles: this.audioFiles,
otherFiles: this.otherFiles,
audioFiles: (this.audioFiles || []).map(audioFile => audioFile.toJSON()),
otherFiles: (this.otherFiles || []).map(otherFile => otherFile.toJSON()),
tags: this.tags,
book: this.bookToJSON(),
tracks: this.tracksToJSON()
@ -175,14 +178,46 @@ class Audiobook {
return false
}
// Update was made to add ino values, ensure they are set
async checkUpdateInos() {
var hasUpdates = false
if (!this.ino) {
this.ino = await getIno(this.fullPath)
hasUpdates = true
}
for (let i = 0; i < this.audioFiles.length; i++) {
var af = this.audioFiles[i]
if (!af.ino || af.ino === this.ino) {
af.ino = await getIno(af.fullPath)
if (!af.ino) {
Logger.error('[Audiobook] checkUpdateInos: Failed to set ino for audio file', af.fullPath)
} else {
var track = this.tracks.find(t => comparePaths(t.path, af.path))
if (track) {
track.ino = af.ino
}
}
hasUpdates = true
}
}
return hasUpdates
}
setData(data) {
this.id = (Math.trunc(Math.random() * 1000) + Date.now()).toString(36)
this.ino = data.ino || null
this.path = data.path
this.fullPath = data.fullPath
this.addedAt = Date.now()
this.lastUpdate = this.addedAt
this.otherFiles = data.otherFiles || []
if (data.otherFiles) {
data.otherFiles.forEach((file) => {
this.addOtherFile(file)
})
}
this.setBook(data)
}
@ -198,6 +233,20 @@ class Audiobook {
return track
}
addAudioFile(audioFileData) {
var audioFile = new AudioFile()
audioFile.setData(audioFileData)
this.audioFiles.push(audioFile)
return audioFile
}
addOtherFile(fileData) {
var file = new AudiobookFile()
file.setData(fileData)
this.otherFiles.push(file)
return file
}
update(payload) {
var hasUpdates = false
@ -241,17 +290,12 @@ class Audiobook {
}
removeAudioFile(audioFile) {
this.tracks = this.tracks.filter(t => t.path !== audioFile.path)
this.audioFiles = this.audioFiles.filter(f => f.path !== audioFile.path)
}
audioPartExists(part) {
var path = Path.join(this.path, part)
return this.audioFiles.find(file => file.path === path)
this.tracks = this.tracks.filter(t => t.ino !== audioFile.ino)
this.audioFiles = this.audioFiles.filter(f => f.ino !== audioFile.ino)
}
checkUpdateMissingParts() {
var currMissingParts = this.missingParts.join(',')
var currMissingParts = (this.missingParts || []).join(',') || ''
var current_index = 1
var missingParts = []
@ -268,7 +312,8 @@ class Audiobook {
this.missingParts = missingParts
var wasUpdated = this.missingParts.join(',') !== currMissingParts
var newMissingParts = (this.missingParts || []).join(',') || ''
var wasUpdated = newMissingParts !== currMissingParts
if (wasUpdated && this.missingParts.length) {
Logger.info(`[Audiobook] "${this.title}" has ${missingParts.length} missing parts`)
}
@ -282,16 +327,18 @@ class Audiobook {
var newOtherFilePaths = newOtherFiles.map(f => f.path)
this.otherFiles = this.otherFiles.filter(f => newOtherFilePaths.includes(f.path))
newOtherFiles.forEach((file) => {
var existingOtherFile = this.otherFiles.find(f => f.path === file.path)
if (!existingOtherFile) {
Logger.info(`[Audiobook] New other file found on sync ${file.filename}/${file.filetype} | "${this.title}"`)
this.otherFiles.push(file)
Logger.debug(`[Audiobook] New other file found on sync ${file.filename}/${file.filetype} | "${this.title}"`)
this.addOtherFile(file)
}
})
var hasUpdates = currOtherFileNum !== this.otherFiles.length
// Check if cover was a local image and that it still exists
var imageFiles = this.otherFiles.filter(f => f.filetype === 'image')
if (this.book.cover && this.book.cover.substr(1).startsWith('local')) {
var coverStillExists = imageFiles.find(f => comparePaths(f.path, this.book.cover.substr('/local/'.length)))
@ -302,6 +349,7 @@ class Audiobook {
}
}
// If no cover set and image file exists then use it
if (!this.book.cover && imageFiles.length) {
this.book.cover = Path.join('/local', imageFiles[0].path)
Logger.info(`[Audiobook] Local cover was set | "${this.title}"`)
@ -310,8 +358,38 @@ class Audiobook {
return hasUpdates
}
syncAudioFile(audioFile, fileScanData) {
var hasUpdates = audioFile.syncFile(fileScanData)
if (hasUpdates) {
var track = this.tracks.find(t => t.ino === audioFile.ino)
if (track) {
track.syncFile(fileScanData)
}
}
return hasUpdates
}
syncPaths(audiobookData) {
var hasUpdates = false
var keysToSync = ['path', 'fullPath']
keysToSync.forEach((key) => {
if (audiobookData[key] !== undefined && audiobookData[key] !== this[key]) {
hasUpdates = true
this[key] = audiobookData[key]
}
})
if (hasUpdates) {
this.book.syncPathsUpdated(audiobookData)
}
return hasUpdates
}
isSearchMatch(search) {
return this.book.isSearchMatch(search.toLowerCase().trim())
}
getAudioFileByIno(ino) {
return this.audioFiles.find(af => af.ino === ino)
}
}
module.exports = Audiobook

48
server/AudiobookFile.js Normal file
View file

@ -0,0 +1,48 @@
class AudiobookFile {
constructor(data) {
this.ino = null
this.filetype = null
this.filename = null
this.ext = null
this.path = null
this.fullPath = null
this.addedAt = null
if (data) {
this.construct(data)
}
}
toJSON() {
return {
ino: this.ino || null,
filetype: this.filetype,
filename: this.filename,
ext: this.ext,
path: this.path,
fullPath: this.fullPath,
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.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.addedAt = Date.now()
}
}
module.exports = AudiobookFile

View file

@ -129,6 +129,18 @@ class Book {
return hasUpdates
}
// 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']
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._author.toLowerCase().includes(search) || this._series.toLowerCase().includes(search)
}

View file

@ -33,6 +33,11 @@ class Logger {
console.info(`[${this.timestamp}] INFO:`, ...args)
}
note(...args) {
if (this.LogLevel > LOG_LEVEL.INFO) return
console.log(`[${this.timestamp}] NOTE:`, ...args)
}
warn(...args) {
if (this.LogLevel > LOG_LEVEL.WARN) return
console.warn(`[${this.timestamp}] WARN:`, ...args)

View file

@ -3,6 +3,7 @@ const BookFinder = require('./BookFinder')
const Audiobook = require('./Audiobook')
const audioFileScanner = require('./utils/audioFileScanner')
const { getAllAudiobookFiles } = require('./utils/scandir')
const { comparePaths, getIno } = require('./utils/index')
const { secondsToTimestamp } = require('./utils/fileUtils')
class Scanner {
@ -21,12 +22,58 @@ class Scanner {
return this.db.audiobooks
}
async setAudiobookDataInos(audiobookData) {
for (let i = 0; i < audiobookData.length; i++) {
var abd = audiobookData[i]
var matchingAB = this.db.audiobooks.find(_ab => comparePaths(_ab.path, abd.path))
if (matchingAB) {
if (!matchingAB.ino) {
matchingAB.ino = await getIno(matchingAB.fullPath)
}
abd.ino = matchingAB.ino
} else {
abd.ino = await getIno(abd.fullPath)
if (!abd.ino) {
Logger.error('[Scanner] Invalid ino - ignoring audiobook data', abd.path)
}
}
}
return audiobookData.filter(abd => !!abd.ino)
}
async setAudioFileInos(audiobookDataAudioFiles, audiobookAudioFiles) {
for (let i = 0; i < audiobookDataAudioFiles.length; i++) {
var abdFile = audiobookDataAudioFiles[i]
var matchingFile = audiobookAudioFiles.find(af => comparePaths(af.path, abdFile.path))
if (matchingFile) {
if (!matchingFile.ino) {
matchingFile.ino = await getIno(matchingFile.fullPath)
}
abdFile.ino = matchingFile.ino
} else {
abdFile.ino = await getIno(abdFile.fullPath)
if (!abdFile.ino) {
Logger.error('[Scanner] Invalid abdFile ino - ignoring abd audio file', abdFile.path)
}
}
}
return audiobookDataAudioFiles.filter(abdFile => !!abdFile.ino)
}
async scan() {
// TEMP - fix relative file paths
// TEMP - update ino for each audiobook
if (this.audiobooks.length) {
for (let i = 0; i < this.audiobooks.length; i++) {
var ab = this.audiobooks[i]
if (ab.fixRelativePath(this.AudiobookPath)) {
var shouldUpdate = ab.fixRelativePath(this.AudiobookPath) || !ab.ino
// Update ino if an audio file has the same ino as the audiobook
var shouldUpdateIno = !ab.ino || (ab.audioFiles || []).find(abf => abf.ino === ab.ino)
if (shouldUpdateIno) {
await ab.checkUpdateInos()
}
if (shouldUpdate) {
await this.db.updateAudiobook(ab)
}
}
@ -35,6 +82,9 @@ class Scanner {
const scanStart = Date.now()
var audiobookDataFound = await getAllAudiobookFiles(this.AudiobookPath)
// Set ino for each ab data as a string
audiobookDataFound = await this.setAudiobookDataInos(audiobookDataFound)
if (this.cancelScan) {
this.cancelScan = false
return null
@ -48,17 +98,13 @@ class Scanner {
// Check for removed audiobooks
for (let i = 0; i < this.audiobooks.length; i++) {
var dataFound = audiobookDataFound.find(abd => abd.path === this.audiobooks[i].path)
var dataFound = audiobookDataFound.find(abd => abd.ino === this.audiobooks[i].ino)
if (!dataFound) {
Logger.info(`[Scanner] Removing audiobook "${this.audiobooks[i].title}" - no longer in dir`)
var audiobookJSON = this.audiobooks[i].toJSONMinified()
await this.db.removeEntity('audiobook', this.audiobooks[i].id)
if (!this.audiobooks[i]) {
Logger.error('[Scanner] Oops... audiobook is now invalid...')
continue;
}
scanResults.removed++
this.emitter('audiobook_removed', this.audiobooks[i].toJSONMinified())
this.emitter('audiobook_removed', audiobookJSON)
}
if (this.cancelScan) {
this.cancelScan = false
@ -68,38 +114,44 @@ class Scanner {
for (let i = 0; i < audiobookDataFound.length; i++) {
var audiobookData = audiobookDataFound[i]
var existingAudiobook = this.audiobooks.find(a => a.fullPath === audiobookData.fullPath)
if (existingAudiobook) {
Logger.debug(`[Scanner] Audiobook already added, check updates for "${existingAudiobook.title}"`)
var existingAudiobook = this.audiobooks.find(a => a.ino === audiobookData.ino)
Logger.debug(`[Scanner] Scanning "${audiobookData.title}" (${audiobookData.ino}) - ${!!existingAudiobook ? 'Exists' : 'New'}`)
if (!audiobookData.parts.length) {
if (existingAudiobook) {
if (!audiobookData.audioFiles.length) {
Logger.error(`[Scanner] "${existingAudiobook.title}" no valid audio files found - removing audiobook`)
await this.db.removeEntity('audiobook', existingAudiobook.id)
this.emitter('audiobook_removed', existingAudiobook.toJSONMinified())
scanResults.removed++
} else {
audiobookData.audioFiles = await this.setAudioFileInos(audiobookData.audioFiles, existingAudiobook.audioFiles)
var abdAudioFileInos = audiobookData.audioFiles.map(af => af.ino)
// Check for audio files that were removed
var removedAudioFiles = existingAudiobook.audioFiles.filter(file => !audiobookData.parts.includes(file.filename))
var removedAudioFiles = existingAudiobook.audioFiles.filter(file => !abdAudioFileInos.includes(file.ino))
if (removedAudioFiles.length) {
Logger.info(`[Scanner] ${removedAudioFiles.length} audio files removed for audiobook "${existingAudiobook.title}"`)
removedAudioFiles.forEach((af) => existingAudiobook.removeAudioFile(af))
}
// Check for audio files that were added
var newParts = audiobookData.parts.filter(part => !existingAudiobook.audioPartExists(part))
if (newParts.length) {
Logger.info(`[Scanner] ${newParts.length} new audio parts were found for audiobook "${existingAudiobook.title}"`)
// If previously invalid part, remove from invalid list because it will be re-scanned
newParts.forEach((part) => {
if (existingAudiobook.invalidParts.includes(part)) {
existingAudiobook.invalidParts = existingAudiobook.invalidParts.filter(p => p !== part)
// Check for new audio files and sync existing audio files
var newAudioFiles = []
var hasUpdatedAudioFiles = false
audiobookData.audioFiles.forEach((file) => {
var existingAudioFile = existingAudiobook.getAudioFileByIno(file.ino)
if (existingAudioFile) { // Audio file exists, sync paths
if (existingAudiobook.syncAudioFile(existingAudioFile, file)) {
hasUpdatedAudioFiles = true
}
})
// Scan new audio parts found
await audioFileScanner.scanParts(existingAudiobook, newParts)
} else {
newAudioFiles.push(file)
}
})
if (newAudioFiles.length) {
Logger.info(`[Scanner] ${newAudioFiles.length} new audio files were found for audiobook "${existingAudiobook.title}"`)
// Scan new audio files found
await audioFileScanner.scanAudioFiles(existingAudiobook, newAudioFiles)
}
if (!existingAudiobook.tracks.length) {
@ -108,7 +160,7 @@ class Scanner {
await this.db.removeEntity('audiobook', existingAudiobook.id)
this.emitter('audiobook_removed', existingAudiobook.toJSONMinified())
} else {
var hasUpdates = removedAudioFiles.length || newParts.length
var hasUpdates = removedAudioFiles.length || newAudioFiles.length || hasUpdatedAudioFiles
if (existingAudiobook.checkUpdateMissingParts()) {
Logger.info(`[Scanner] "${existingAudiobook.title}" missing parts updated`)
@ -119,6 +171,11 @@ class Scanner {
hasUpdates = true
}
// Syncs path and fullPath
if (existingAudiobook.syncPaths(audiobookData)) {
hasUpdates = true
}
if (hasUpdates) {
Logger.info(`[Scanner] "${existingAudiobook.title}" was updated - saving`)
existingAudiobook.lastUpdate = Date.now()
@ -129,12 +186,12 @@ class Scanner {
}
} // end if update existing
} else {
if (!audiobookData.parts.length) {
Logger.error('[Scanner] No valid audio tracks for Audiobook', audiobookData)
if (!audiobookData.audioFiles.length) {
Logger.error('[Scanner] No valid audio tracks for Audiobook', audiobookData.path)
} else {
var audiobook = new Audiobook()
audiobook.setData(audiobookData)
await audioFileScanner.scanParts(audiobook, audiobookData.parts)
await audioFileScanner.scanAudioFiles(audiobook, audiobookData.audioFiles)
if (!audiobook.tracks.length) {
Logger.warn('[Scanner] Invalid audiobook, no valid tracks', audiobook.title)
} else {

View file

@ -18,9 +18,9 @@ class Server {
constructor(PORT, CONFIG_PATH, METADATA_PATH, AUDIOBOOK_PATH) {
this.Port = PORT
this.Host = '0.0.0.0'
this.ConfigPath = CONFIG_PATH
this.AudiobookPath = AUDIOBOOK_PATH
this.MetadataPath = METADATA_PATH
this.ConfigPath = Path.normalize(CONFIG_PATH)
this.AudiobookPath = Path.normalize(AUDIOBOOK_PATH)
this.MetadataPath = Path.normalize(METADATA_PATH)
fs.ensureDirSync(CONFIG_PATH)
fs.ensureDirSync(METADATA_PATH)

View file

@ -1,6 +1,6 @@
var EventEmitter = require('events')
var Logger = require('./Logger')
var chokidar = require('chokidar')
var Watcher = require('watcher')
class FolderWatcher extends EventEmitter {
constructor(audiobookPath) {
@ -12,15 +12,14 @@ class FolderWatcher extends EventEmitter {
initWatcher() {
try {
Logger.info('[WATCHER] Initializing..')
this.watcher = chokidar.watch(this.AudiobookPath, {
ignoreInitial: true,
Logger.info('[FolderWatcher] Initializing..')
this.watcher = new Watcher(this.AudiobookPath, {
ignored: /(^|[\/\\])\../, // ignore dotfiles
persistent: true,
awaitWriteFinish: {
stabilityThreshold: 2500,
pollInterval: 500
}
renameDetection: true,
renameTimeout: 2000,
recursive: true,
ignoreInitial: true,
persistent: true
})
this.watcher
.on('add', (path) => {
@ -29,10 +28,12 @@ class FolderWatcher extends EventEmitter {
this.onFileUpdated(path)
}).on('unlink', path => {
this.onFileRemoved(path)
}).on('rename', (path, pathNext) => {
this.onRename(path, pathNext)
}).on('error', (error) => {
Logger.error(`Watcher error: ${error}`)
Logger.error(`[FolderWatcher] ${error}`)
}).on('ready', () => {
Logger.info('[WATCHER] Ready')
Logger.info('[FolderWatcher] Ready')
})
} catch (error) {
Logger.error('Chokidar watcher failed', error)
@ -53,7 +54,7 @@ class FolderWatcher extends EventEmitter {
}
onFileRemoved(path) {
Logger.debug('FolderWatcher: File Removed', path)
Logger.debug('[FolderWatcher] File Removed', path)
this.emit('file_removed', {
path: path.replace(this.AudiobookPath, ''),
fullPath: path
@ -61,11 +62,15 @@ class FolderWatcher extends EventEmitter {
}
onFileUpdated(path) {
Logger.debug('FolderWatcher: Updated File', path)
Logger.debug('[FolderWatcher] Updated File', path)
this.emit('file_updated', {
path: path.replace(this.AudiobookPath, ''),
fullPath: path
})
}
onRename(pathFrom, pathTo) {
Logger.debug(`[FolderWatcher] Rename ${pathFrom} => ${pathTo}`)
}
}
module.exports = FolderWatcher

View file

@ -26,6 +26,12 @@ class OpenLibrary {
async getWorksData(worksKey) {
var worksData = await this.get(`${worksKey}.json`)
if (!worksData) {
return {
errorMsg: 'Works Data Request failed',
errorCode: 500
}
}
if (!worksData.covers) worksData.covers = []
var coverImages = worksData.covers.filter(c => c > 0).map(c => `https://covers.openlibrary.org/b/id/${c}-L.jpg`)
var description = null

View file

@ -1,6 +1,7 @@
const Path = require('path')
const Logger = require('../Logger')
var prober = require('./prober')
const prober = require('./prober')
const AudioFile = require('../AudioFile')
function getDefaultAudioStream(audioStreams) {
@ -76,41 +77,42 @@ function getTrackNumberFromFilename(filename) {
return number
}
async function scanParts(audiobook, parts) {
if (!parts || !parts.length) {
Logger.error('[AudioFileScanner] Scan Parts', audiobook.title, 'No Parts', parts)
async function scanAudioFiles(audiobook, newAudioFiles) {
if (!newAudioFiles || !newAudioFiles.length) {
Logger.error('[AudioFileScanner] Scan Audio Files no files', audiobook.title)
return
}
var tracks = []
for (let i = 0; i < parts.length; i++) {
var fullPath = Path.join(audiobook.fullPath, parts[i])
for (let i = 0; i < newAudioFiles.length; i++) {
var audioFile = newAudioFiles[i]
var scanData = await scan(fullPath)
var scanData = await scan(audioFile.fullPath)
if (!scanData || scanData.error) {
Logger.error('[AudioFileScanner] Scan failed for', parts[i])
audiobook.invalidParts.push(parts[i])
Logger.error('[AudioFileScanner] Scan failed for', audioFile.path)
// audiobook.invalidAudioFiles.push(parts[i])
continue;
}
var trackNumFromMeta = getTrackNumberFromMeta(scanData)
var trackNumFromFilename = getTrackNumberFromFilename(parts[i])
var trackNumFromFilename = getTrackNumberFromFilename(audioFile.filename)
var audioFileObj = {
path: Path.join(audiobook.path, parts[i]),
ext: Path.extname(parts[i]),
filename: parts[i],
fullPath: fullPath,
ino: audioFile.ino,
filename: audioFile.filename,
path: audioFile.path,
fullPath: audioFile.fullPath,
ext: audioFile.ext,
...scanData,
trackNumFromMeta,
trackNumFromFilename
}
audiobook.audioFiles.push(audioFileObj)
audiobook.addAudioFile(audioFileObj)
var trackNumber = 1
if (parts.length > 1) {
if (newAudioFiles.length > 1) {
trackNumber = isNumber(trackNumFromMeta) ? trackNumFromMeta : trackNumFromFilename
if (trackNumber === null) {
Logger.error('[AudioFileScanner] Invalid track number for', parts[i])
Logger.error('[AudioFileScanner] Invalid track number for', audioFile.filename)
audioFileObj.invalid = true
audioFileObj.error = 'Failed to get track number'
continue;
@ -118,7 +120,7 @@ async function scanParts(audiobook, parts) {
}
if (tracks.find(t => t.index === trackNumber)) {
Logger.error('[AudioFileScanner] Duplicate track number for', parts[i])
Logger.error('[AudioFileScanner] Duplicate track number for', audioFile.filename)
audioFileObj.invalid = true
audioFileObj.error = 'Duplicate track number'
continue;
@ -156,4 +158,4 @@ async function scanParts(audiobook, parts) {
audiobook.tracks.sort((a, b) => a.index - b.index)
}
}
module.exports.scanParts = scanParts
module.exports.scanAudioFiles = scanAudioFiles

View file

@ -1,4 +1,6 @@
const Path = require('path')
const fs = require('fs')
const Logger = require('../Logger')
const levenshteinDistance = (str1, str2, caseSensitive = false) => {
if (!caseSensitive) {
@ -48,24 +50,13 @@ module.exports.isObject = (val) => {
return val !== null && typeof val === 'object'
}
function normalizePath(path) {
const replace = [
[/\\/g, '/'],
[/(\w):/, '/$1'],
[/(\w+)\/\.\.\/?/g, ''],
[/^\.\//, ''],
[/\/\.\//, '/'],
[/\/\.$/, ''],
[/\/$/, ''],
]
replace.forEach(array => {
while (array[0].test(path)) {
path = path.replace(array[0], array[1])
}
})
return path
module.exports.comparePaths = (path1, path2) => {
return path1 === path2 || Path.normalize(path1) === Path.normalize(path2)
}
module.exports.comparePaths = (path1, path2) => {
return (path1 === path2) || (normalizePath(path1) === normalizePath(path2))
module.exports.getIno = (path) => {
return fs.promises.stat(path, { bigint: true }).then((data => String(data.ino))).catch((err) => {
Logger.error('[Utils] Failed to get ino for path', path, error)
return null
})
}

View file

@ -3,7 +3,7 @@ const dir = require('node-dir')
const Logger = require('../Logger')
const { cleanString } = require('./index')
const AUDIOBOOK_PARTS_FORMATS = ['m4b', 'mp3']
const AUDIO_FORMATS = ['m4b', 'mp3']
const INFO_FORMATS = ['nfo']
const IMAGE_FORMATS = ['png', 'jpg', 'jpeg', 'webp']
const EBOOK_FORMATS = ['epub', 'pdf']
@ -23,7 +23,7 @@ function getPaths(path) {
function getFileType(ext) {
var ext_cleaned = ext.toLowerCase()
if (ext_cleaned.startsWith('.')) ext_cleaned = ext_cleaned.slice(1)
if (AUDIOBOOK_PARTS_FORMATS.includes(ext_cleaned)) return 'abpart'
if (AUDIO_FORMATS.includes(ext_cleaned)) return 'audio'
if (INFO_FORMATS.includes(ext_cleaned)) return 'info'
if (IMAGE_FORMATS.includes(ext_cleaned)) return 'image'
if (EBOOK_FORMATS.includes(ext_cleaned)) return 'ebook'
@ -35,7 +35,7 @@ async function getAllAudiobookFiles(abRootPath) {
var audiobooks = {}
paths.files.forEach((filepath) => {
var relpath = filepath.replace(abRootPath, '').slice(1)
var relpath = Path.normalize(filepath).replace(abRootPath, '').slice(1)
var pathformat = Path.parse(relpath)
var path = pathformat.dir
@ -71,22 +71,20 @@ async function getAllAudiobookFiles(abRootPath) {
publishYear: publishYear,
path: path,
fullPath: Path.join(abRootPath, path),
parts: [],
audioFiles: [],
otherFiles: []
}
}
var filetype = getFileType(pathformat.ext)
if (filetype === 'abpart') {
audiobooks[path].parts.push(pathformat.base)
var fileObj = {
filetype: getFileType(pathformat.ext),
filename: pathformat.base,
path: relpath,
fullPath: filepath,
ext: pathformat.ext
}
if (fileObj.filetype === 'audio') {
audiobooks[path].audioFiles.push(fileObj)
} else {
var fileObj = {
filetype: filetype,
filename: pathformat.base,
path: relpath,
fullPath: filepath,
ext: pathformat.ext
}
audiobooks[path].otherFiles.push(fileObj)
}
})