mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-11-07 16:44:16 +01:00
Auto add/update/remove audiobooks, update screenshots
This commit is contained in:
parent
2e82370408
commit
07a2a0aefd
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "1.0.8",
|
"version": "1.1.0",
|
||||||
"description": "Audiobook manager and player",
|
"description": "Audiobook manager and player",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 196 KiB After Width: | Height: | Size: 168 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.2 MiB |
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "1.0.8",
|
"version": "1.1.0",
|
||||||
"description": "Self-hosted audiobook server for managing and playing audiobooks.",
|
"description": "Self-hosted audiobook server for managing and playing audiobooks.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -28,8 +28,6 @@ will store "With a Subtitle" as the subtitle
|
|||||||
|
|
||||||
#### Features coming soon:
|
#### Features coming soon:
|
||||||
|
|
||||||
* Auto add and update audiobooks (currently you need to press scan)
|
|
||||||
* User permissions & editing users
|
|
||||||
* Support different views to see more details of each audiobook
|
* Support different views to see more details of each audiobook
|
||||||
* Option to download all files in a zip file
|
* Option to download all files in a zip file
|
||||||
* iOS App (Android is in beta [here](https://play.google.com/store/apps/details?id=com.audiobookshelf.app))
|
* iOS App (Android is in beta [here](https://play.google.com/store/apps/details?id=com.audiobookshelf.app))
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
|
const fs = require('fs-extra')
|
||||||
const Logger = require('./Logger')
|
const Logger = require('./Logger')
|
||||||
const BookFinder = require('./BookFinder')
|
const BookFinder = require('./BookFinder')
|
||||||
const Audiobook = require('./objects/Audiobook')
|
const Audiobook = require('./objects/Audiobook')
|
||||||
const audioFileScanner = require('./utils/audioFileScanner')
|
const audioFileScanner = require('./utils/audioFileScanner')
|
||||||
const { getAllAudiobookFiles } = require('./utils/scandir')
|
const { getAllAudiobookFileData, getAudiobookFileData } = require('./utils/scandir')
|
||||||
const { comparePaths, getIno } = require('./utils/index')
|
const { comparePaths, getIno } = require('./utils/index')
|
||||||
const { secondsToTimestamp } = require('./utils/fileUtils')
|
const { secondsToTimestamp } = require('./utils/fileUtils')
|
||||||
|
const { ScanResult } = require('./utils/constants')
|
||||||
|
|
||||||
|
|
||||||
class Scanner {
|
class Scanner {
|
||||||
constructor(AUDIOBOOK_PATH, METADATA_PATH, db, emitter) {
|
constructor(AUDIOBOOK_PATH, METADATA_PATH, db, emitter) {
|
||||||
@ -60,6 +63,110 @@ class Scanner {
|
|||||||
return audiobookDataAudioFiles.filter(abdFile => !!abdFile.ino)
|
return audiobookDataAudioFiles.filter(abdFile => !!abdFile.ino)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async scanAudiobookData(audiobookData) {
|
||||||
|
var existingAudiobook = this.audiobooks.find(a => a.ino === audiobookData.ino)
|
||||||
|
Logger.debug(`[Scanner] Scanning "${audiobookData.title}" (${audiobookData.ino}) - ${!!existingAudiobook ? 'Exists' : 'New'}`)
|
||||||
|
|
||||||
|
if (existingAudiobook) {
|
||||||
|
|
||||||
|
// REMOVE: No valid audio files
|
||||||
|
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())
|
||||||
|
|
||||||
|
return ScanResult.REMOVED
|
||||||
|
}
|
||||||
|
|
||||||
|
audiobookData.audioFiles = await this.setAudioFileInos(audiobookData.audioFiles, existingAudiobook.audioFiles)
|
||||||
|
|
||||||
|
// Check for audio files that were removed
|
||||||
|
var abdAudioFileInos = audiobookData.audioFiles.map(af => af.ino)
|
||||||
|
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 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
|
||||||
|
}
|
||||||
|
} 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 - sets tracks
|
||||||
|
await audioFileScanner.scanAudioFiles(existingAudiobook, newAudioFiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// REMOVE: No valid audio tracks
|
||||||
|
if (!existingAudiobook.tracks.length) {
|
||||||
|
Logger.error(`[Scanner] "${existingAudiobook.title}" has no valid tracks after update - removing audiobook`)
|
||||||
|
|
||||||
|
await this.db.removeEntity('audiobook', existingAudiobook.id)
|
||||||
|
this.emitter('audiobook_removed', existingAudiobook.toJSONMinified())
|
||||||
|
return ScanResult.REMOVED
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasUpdates = removedAudioFiles.length || newAudioFiles.length || hasUpdatedAudioFiles
|
||||||
|
|
||||||
|
if (existingAudiobook.checkUpdateMissingParts()) {
|
||||||
|
Logger.info(`[Scanner] "${existingAudiobook.title}" missing parts updated`)
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingAudiobook.syncOtherFiles(audiobookData.otherFiles)) {
|
||||||
|
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()
|
||||||
|
await this.db.updateAudiobook(existingAudiobook)
|
||||||
|
this.emitter('audiobook_updated', existingAudiobook.toJSONMinified())
|
||||||
|
|
||||||
|
return ScanResult.UPDATED
|
||||||
|
}
|
||||||
|
|
||||||
|
return ScanResult.UPTODATE
|
||||||
|
}
|
||||||
|
|
||||||
|
// NEW: Check new audiobook
|
||||||
|
if (!audiobookData.audioFiles.length) {
|
||||||
|
Logger.error('[Scanner] No valid audio tracks for Audiobook', audiobookData.path)
|
||||||
|
return ScanResult.NOTHING
|
||||||
|
}
|
||||||
|
|
||||||
|
var audiobook = new Audiobook()
|
||||||
|
audiobook.setData(audiobookData)
|
||||||
|
await audioFileScanner.scanAudioFiles(audiobook, audiobookData.audioFiles)
|
||||||
|
if (!audiobook.tracks.length) {
|
||||||
|
Logger.warn('[Scanner] Invalid audiobook, no valid tracks', audiobook.title)
|
||||||
|
return ScanResult.NOTHING
|
||||||
|
}
|
||||||
|
|
||||||
|
audiobook.checkUpdateMissingParts()
|
||||||
|
Logger.info(`[Scanner] Audiobook "${audiobook.title}" Scanned (${audiobook.sizePretty}) [${audiobook.durationPretty}]`)
|
||||||
|
await this.db.insertAudiobook(audiobook)
|
||||||
|
this.emitter('audiobook_added', audiobook.toJSONMinified())
|
||||||
|
return ScanResult.ADDED
|
||||||
|
}
|
||||||
|
|
||||||
async scan() {
|
async scan() {
|
||||||
// TEMP - fix relative file paths
|
// TEMP - fix relative file paths
|
||||||
// TEMP - update ino for each audiobook
|
// TEMP - update ino for each audiobook
|
||||||
@ -80,7 +187,7 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const scanStart = Date.now()
|
const scanStart = Date.now()
|
||||||
var audiobookDataFound = await getAllAudiobookFiles(this.AudiobookPath, this.db.serverSettings)
|
var audiobookDataFound = await getAllAudiobookFileData(this.AudiobookPath, this.db.serverSettings)
|
||||||
|
|
||||||
// Set ino for each ab data as a string
|
// Set ino for each ab data as a string
|
||||||
audiobookDataFound = await this.setAudiobookDataInos(audiobookDataFound)
|
audiobookDataFound = await this.setAudiobookDataInos(audiobookDataFound)
|
||||||
@ -112,97 +219,14 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for new and updated audiobooks
|
||||||
for (let i = 0; i < audiobookDataFound.length; i++) {
|
for (let i = 0; i < audiobookDataFound.length; i++) {
|
||||||
var audiobookData = audiobookDataFound[i]
|
var audiobookData = audiobookDataFound[i]
|
||||||
var existingAudiobook = this.audiobooks.find(a => a.ino === audiobookData.ino)
|
var result = await this.scanAudiobookData(audiobookData)
|
||||||
Logger.debug(`[Scanner] Scanning "${audiobookData.title}" (${audiobookData.ino}) - ${!!existingAudiobook ? 'Exists' : 'New'}`)
|
if (result === ScanResult.ADDED) scanResults.added++
|
||||||
|
if (result === ScanResult.REMOVED) scanResults.removed++
|
||||||
|
if (result === ScanResult.UPDATED) scanResults.updated++
|
||||||
|
|
||||||
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 => !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 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
|
|
||||||
}
|
|
||||||
} 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) {
|
|
||||||
Logger.error(`[Scanner] "${existingAudiobook.title}" has no valid tracks after update - removing audiobook`)
|
|
||||||
|
|
||||||
await this.db.removeEntity('audiobook', existingAudiobook.id)
|
|
||||||
this.emitter('audiobook_removed', existingAudiobook.toJSONMinified())
|
|
||||||
} else {
|
|
||||||
var hasUpdates = removedAudioFiles.length || newAudioFiles.length || hasUpdatedAudioFiles
|
|
||||||
|
|
||||||
if (existingAudiobook.checkUpdateMissingParts()) {
|
|
||||||
Logger.info(`[Scanner] "${existingAudiobook.title}" missing parts updated`)
|
|
||||||
hasUpdates = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingAudiobook.syncOtherFiles(audiobookData.otherFiles)) {
|
|
||||||
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()
|
|
||||||
await this.db.updateAudiobook(existingAudiobook)
|
|
||||||
this.emitter('audiobook_updated', existingAudiobook.toJSONMinified())
|
|
||||||
scanResults.updated++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // end if update existing
|
|
||||||
} else {
|
|
||||||
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.scanAudioFiles(audiobook, audiobookData.audioFiles)
|
|
||||||
if (!audiobook.tracks.length) {
|
|
||||||
Logger.warn('[Scanner] Invalid audiobook, no valid tracks', audiobook.title)
|
|
||||||
} else {
|
|
||||||
audiobook.checkUpdateMissingParts()
|
|
||||||
Logger.info(`[Scanner] Audiobook "${audiobook.title}" Scanned (${audiobook.sizePretty}) [${audiobook.durationPretty}]`)
|
|
||||||
await this.db.insertAudiobook(audiobook)
|
|
||||||
this.emitter('audiobook_added', audiobook.toJSONMinified())
|
|
||||||
scanResults.added++
|
|
||||||
}
|
|
||||||
} // end if add new
|
|
||||||
}
|
|
||||||
var progress = Math.round(100 * (i + 1) / audiobookDataFound.length)
|
var progress = Math.round(100 * (i + 1) / audiobookDataFound.length)
|
||||||
this.emitter('scan_progress', {
|
this.emitter('scan_progress', {
|
||||||
scanType: 'files',
|
scanType: 'files',
|
||||||
@ -222,6 +246,29 @@ class Scanner {
|
|||||||
return scanResults
|
return scanResults
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async scanAudiobook(audiobookPath) {
|
||||||
|
var exists = await fs.pathExists(audiobookPath)
|
||||||
|
if (!exists) {
|
||||||
|
// Audiobook was deleted, TODO: Should confirm this better
|
||||||
|
var audiobook = this.db.audiobooks.find(ab => ab.fullPath === audiobookPath)
|
||||||
|
if (audiobook) {
|
||||||
|
var audiobookJSON = audiobook.toJSONMinified()
|
||||||
|
await this.db.removeEntity('audiobook', audiobook.id)
|
||||||
|
this.emitter('audiobook_removed', audiobookJSON)
|
||||||
|
return ScanResult.REMOVED
|
||||||
|
}
|
||||||
|
Logger.warn('Path was deleted but no audiobook found', audiobookPath)
|
||||||
|
return ScanResult.NOTHING
|
||||||
|
}
|
||||||
|
|
||||||
|
var audiobookData = await getAudiobookFileData(this.AudiobookPath, audiobookPath, this.db.serverSettings)
|
||||||
|
if (!audiobookData) {
|
||||||
|
return ScanResult.NOTHING
|
||||||
|
}
|
||||||
|
audiobookData.ino = await getIno(audiobookData.fullPath)
|
||||||
|
return this.scanAudiobookData(audiobookData)
|
||||||
|
}
|
||||||
|
|
||||||
async fetchMetadata(id, trackIndex = 0) {
|
async fetchMetadata(id, trackIndex = 0) {
|
||||||
var audiobook = this.audiobooks.find(a => a.id === id)
|
var audiobook = this.audiobooks.find(a => a.id === id)
|
||||||
if (!audiobook) {
|
if (!audiobook) {
|
||||||
|
@ -14,6 +14,7 @@ const StreamManager = require('./StreamManager')
|
|||||||
const RssFeeds = require('./RssFeeds')
|
const RssFeeds = require('./RssFeeds')
|
||||||
const DownloadManager = require('./DownloadManager')
|
const DownloadManager = require('./DownloadManager')
|
||||||
const Logger = require('./Logger')
|
const Logger = require('./Logger')
|
||||||
|
const { ScanResult } = require('./utils/constants')
|
||||||
|
|
||||||
class Server {
|
class Server {
|
||||||
constructor(PORT, CONFIG_PATH, METADATA_PATH, AUDIOBOOK_PATH) {
|
constructor(PORT, CONFIG_PATH, METADATA_PATH, AUDIOBOOK_PATH) {
|
||||||
@ -75,8 +76,21 @@ class Server {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fileAddedUpdated({ path, fullPath }) { }
|
async newFilesAdded({ dir, files }) {
|
||||||
async fileRemoved({ path, fullPath }) { }
|
Logger.info(files.length, 'New Files Added in dir', dir)
|
||||||
|
var result = await this.scanner.scanAudiobook(dir)
|
||||||
|
Logger.info('New Files Added result', result)
|
||||||
|
}
|
||||||
|
async filesRemoved({ dir, files }) {
|
||||||
|
Logger.info(files.length, 'Files Removed in dir', dir)
|
||||||
|
var result = await this.scanner.scanAudiobook(dir)
|
||||||
|
Logger.info('Files Removed result', result)
|
||||||
|
}
|
||||||
|
async filesRenamed({ dir, files }) {
|
||||||
|
Logger.info(files.length, 'Files Renamed in dir', dir)
|
||||||
|
var result = await this.scanner.scanAudiobook(dir)
|
||||||
|
Logger.info('Files Renamed result', result)
|
||||||
|
}
|
||||||
|
|
||||||
async scan() {
|
async scan() {
|
||||||
Logger.info('[Server] Starting Scan')
|
Logger.info('[Server] Starting Scan')
|
||||||
@ -112,9 +126,9 @@ class Server {
|
|||||||
this.auth.init()
|
this.auth.init()
|
||||||
|
|
||||||
this.watcher.initWatcher()
|
this.watcher.initWatcher()
|
||||||
this.watcher.on('file_added', this.fileAddedUpdated.bind(this))
|
this.watcher.on('new_files', this.newFilesAdded.bind(this))
|
||||||
this.watcher.on('file_removed', this.fileRemoved.bind(this))
|
this.watcher.on('removed_files', this.filesRemoved.bind(this))
|
||||||
this.watcher.on('file_updated', this.fileAddedUpdated.bind(this))
|
this.watcher.on('renamed_files', this.filesRenamed.bind(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
authMiddleware(req, res, next) {
|
authMiddleware(req, res, next) {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
var EventEmitter = require('events')
|
const Path = require('path')
|
||||||
var Logger = require('./Logger')
|
const EventEmitter = require('events')
|
||||||
var Watcher = require('watcher')
|
const Watcher = require('watcher')
|
||||||
|
const Logger = require('./Logger')
|
||||||
|
const { getIno } = require('./utils/index')
|
||||||
|
|
||||||
class FolderWatcher extends EventEmitter {
|
class FolderWatcher extends EventEmitter {
|
||||||
constructor(audiobookPath) {
|
constructor(audiobookPath) {
|
||||||
@ -8,6 +10,11 @@ class FolderWatcher extends EventEmitter {
|
|||||||
this.AudiobookPath = audiobookPath
|
this.AudiobookPath = audiobookPath
|
||||||
this.folderMap = {}
|
this.folderMap = {}
|
||||||
this.watcher = null
|
this.watcher = null
|
||||||
|
|
||||||
|
this.pendingBatchDelay = 4000
|
||||||
|
|
||||||
|
// Audiobook paths with changes
|
||||||
|
this.pendingBatch = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
initWatcher() {
|
initWatcher() {
|
||||||
@ -46,32 +53,69 @@ class FolderWatcher extends EventEmitter {
|
|||||||
return this.watcher.close()
|
return this.watcher.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
onNewFile(path) {
|
// After [pendingBatchDelay] seconds emit batch
|
||||||
|
async onNewFile(path) {
|
||||||
Logger.debug('FolderWatcher: New File', path)
|
Logger.debug('FolderWatcher: New File', path)
|
||||||
this.emit('file_added', {
|
|
||||||
path: path.replace(this.AudiobookPath, ''),
|
var dir = Path.dirname(path)
|
||||||
fullPath: path
|
if (this.pendingBatch[dir]) {
|
||||||
})
|
this.pendingBatch[dir].files.push(path)
|
||||||
|
clearTimeout(this.pendingBatch[dir].timeout)
|
||||||
|
} else {
|
||||||
|
this.pendingBatch[dir] = {
|
||||||
|
dir,
|
||||||
|
files: [path]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pendingBatch[dir].timeout = setTimeout(() => {
|
||||||
|
this.emit('new_files', this.pendingBatch[dir])
|
||||||
|
delete this.pendingBatch[dir]
|
||||||
|
}, this.pendingBatchDelay)
|
||||||
}
|
}
|
||||||
|
|
||||||
onFileRemoved(path) {
|
onFileRemoved(path) {
|
||||||
Logger.debug('[FolderWatcher] File Removed', path)
|
Logger.debug('[FolderWatcher] File Removed', path)
|
||||||
this.emit('file_removed', {
|
|
||||||
path: path.replace(this.AudiobookPath, ''),
|
var dir = Path.dirname(path)
|
||||||
fullPath: path
|
if (this.pendingBatch[dir]) {
|
||||||
})
|
this.pendingBatch[dir].files.push(path)
|
||||||
|
clearTimeout(this.pendingBatch[dir].timeout)
|
||||||
|
} else {
|
||||||
|
this.pendingBatch[dir] = {
|
||||||
|
dir,
|
||||||
|
files: [path]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pendingBatch[dir].timeout = setTimeout(() => {
|
||||||
|
this.emit('removed_files', this.pendingBatch[dir])
|
||||||
|
delete this.pendingBatch[dir]
|
||||||
|
}, this.pendingBatchDelay)
|
||||||
}
|
}
|
||||||
|
|
||||||
onFileUpdated(path) {
|
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) {
|
onRename(pathFrom, pathTo) {
|
||||||
Logger.debug(`[FolderWatcher] Rename ${pathFrom} => ${pathTo}`)
|
Logger.debug(`[FolderWatcher] Rename ${pathFrom} => ${pathTo}`)
|
||||||
|
|
||||||
|
var dir = Path.dirname(pathTo)
|
||||||
|
if (this.pendingBatch[dir]) {
|
||||||
|
this.pendingBatch[dir].files.push(pathTo)
|
||||||
|
clearTimeout(this.pendingBatch[dir].timeout)
|
||||||
|
} else {
|
||||||
|
this.pendingBatch[dir] = {
|
||||||
|
dir,
|
||||||
|
files: [pathTo]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pendingBatch[dir].timeout = setTimeout(() => {
|
||||||
|
this.emit('renamed_files', this.pendingBatch[dir])
|
||||||
|
delete this.pendingBatch[dir]
|
||||||
|
}, this.pendingBatchDelay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = FolderWatcher
|
module.exports = FolderWatcher
|
7
server/utils/constants.js
Normal file
7
server/utils/constants.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module.exports.ScanResult = {
|
||||||
|
NOTHING: 0,
|
||||||
|
ADDED: 1,
|
||||||
|
UPDATED: 2,
|
||||||
|
REMOVED: 3,
|
||||||
|
UPTODATE: 4
|
||||||
|
}
|
@ -32,17 +32,20 @@ module.exports.levenshteinDistance = levenshteinDistance
|
|||||||
const cleanString = (str) => {
|
const cleanString = (str) => {
|
||||||
if (!str) return ''
|
if (!str) return ''
|
||||||
|
|
||||||
|
// Now supporting all utf-8 characters, can remove this method in future
|
||||||
|
|
||||||
// replace accented characters: https://stackoverflow.com/a/49901740/7431543
|
// replace accented characters: https://stackoverflow.com/a/49901740/7431543
|
||||||
str = str.normalize('NFD').replace(/[\u0300-\u036f]/g, "")
|
// str = str.normalize('NFD').replace(/[\u0300-\u036f]/g, "")
|
||||||
|
|
||||||
const availableChars = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
|
// const availableChars = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
|
||||||
const cleanChar = (char) => availableChars.indexOf(char) < 0 ? '?' : char
|
// const cleanChar = (char) => availableChars.indexOf(char) < 0 ? '?' : char
|
||||||
|
|
||||||
var cleaned = ''
|
// var cleaned = ''
|
||||||
for (let i = 0; i < str.length; i++) {
|
// for (let i = 0; i < str.length; i++) {
|
||||||
cleaned += cleanChar(str[i])
|
// cleaned += cleanChar(str[i])
|
||||||
}
|
// }
|
||||||
return cleaned
|
|
||||||
|
return cleaned.trim()
|
||||||
}
|
}
|
||||||
module.exports.cleanString = cleanString
|
module.exports.cleanString = cleanString
|
||||||
|
|
||||||
|
@ -30,7 +30,54 @@ function getFileType(ext) {
|
|||||||
return 'unknown'
|
return 'unknown'
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAllAudiobookFiles(abRootPath, serverSettings = {}) {
|
// Input relative filepath, output all details that can be parsed
|
||||||
|
function getAudiobookDataFromFilepath(abRootPath, relpath, parseSubtitle = false) {
|
||||||
|
var pathformat = Path.parse(relpath)
|
||||||
|
var path = pathformat.dir
|
||||||
|
|
||||||
|
if (!path) {
|
||||||
|
Logger.error('Ignoring file in root dir', relpath)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// If relative file directory has 3 folders, then the middle folder will be series
|
||||||
|
var splitDir = path.split(Path.sep)
|
||||||
|
var author = null
|
||||||
|
if (splitDir.length > 1) author = splitDir.shift()
|
||||||
|
var series = null
|
||||||
|
if (splitDir.length > 1) series = splitDir.shift()
|
||||||
|
var title = splitDir.shift()
|
||||||
|
|
||||||
|
var publishYear = null
|
||||||
|
var subtitle = null
|
||||||
|
|
||||||
|
// If Title is of format 1999 - Title, then use 1999 as publish year
|
||||||
|
var publishYearMatch = title.match(/^([0-9]{4}) - (.+)/)
|
||||||
|
if (publishYearMatch && publishYearMatch.length > 2) {
|
||||||
|
if (!isNaN(publishYearMatch[1])) {
|
||||||
|
publishYear = publishYearMatch[1]
|
||||||
|
title = publishYearMatch[2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parseSubtitle && title.includes(' - ')) {
|
||||||
|
var splitOnSubtitle = title.split(' - ')
|
||||||
|
title = splitOnSubtitle.shift()
|
||||||
|
subtitle = splitOnSubtitle.join(' - ')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
author,
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
series,
|
||||||
|
publishYear,
|
||||||
|
path, // relative audiobook path i.e. /Author Name/Book Name/..
|
||||||
|
fullPath: Path.join(abRootPath, path) // i.e. /audiobook/Author Name/Book Name/..
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAllAudiobookFileData(abRootPath, serverSettings = {}) {
|
||||||
var parseSubtitle = !!serverSettings.scannerParseSubtitle
|
var parseSubtitle = !!serverSettings.scannerParseSubtitle
|
||||||
|
|
||||||
var paths = await getPaths(abRootPath)
|
var paths = await getPaths(abRootPath)
|
||||||
@ -38,59 +85,26 @@ async function getAllAudiobookFiles(abRootPath, serverSettings = {}) {
|
|||||||
|
|
||||||
paths.files.forEach((filepath) => {
|
paths.files.forEach((filepath) => {
|
||||||
var relpath = Path.normalize(filepath).replace(abRootPath, '').slice(1)
|
var relpath = Path.normalize(filepath).replace(abRootPath, '').slice(1)
|
||||||
var pathformat = Path.parse(relpath)
|
var parsed = Path.parse(relpath)
|
||||||
var path = pathformat.dir
|
var path = parsed.dir
|
||||||
|
|
||||||
if (!path) {
|
|
||||||
Logger.error('Ignoring file in root dir', filepath)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If relative file directory has 3 folders, then the middle folder will be series
|
|
||||||
var splitDir = pathformat.dir.split(Path.sep)
|
|
||||||
var author = null
|
|
||||||
if (splitDir.length > 1) author = splitDir.shift()
|
|
||||||
var series = null
|
|
||||||
if (splitDir.length > 1) series = splitDir.shift()
|
|
||||||
var title = splitDir.shift()
|
|
||||||
|
|
||||||
var publishYear = null
|
|
||||||
var subtitle = null
|
|
||||||
|
|
||||||
// If Title is of format 1999 - Title, then use 1999 as publish year
|
|
||||||
var publishYearMatch = title.match(/^([0-9]{4}) - (.+)/)
|
|
||||||
if (publishYearMatch && publishYearMatch.length > 2) {
|
|
||||||
if (!isNaN(publishYearMatch[1])) {
|
|
||||||
publishYear = publishYearMatch[1]
|
|
||||||
title = publishYearMatch[2]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parseSubtitle && title.includes(' - ')) {
|
|
||||||
var splitOnSubtitle = title.split(' - ')
|
|
||||||
title = splitOnSubtitle.shift()
|
|
||||||
subtitle = splitOnSubtitle.join(' - ')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!audiobooks[path]) {
|
if (!audiobooks[path]) {
|
||||||
|
var audiobookData = getAudiobookDataFromFilepath(abRootPath, relpath, parseSubtitle)
|
||||||
|
if (!audiobookData) return
|
||||||
|
|
||||||
audiobooks[path] = {
|
audiobooks[path] = {
|
||||||
author,
|
...audiobookData,
|
||||||
title,
|
|
||||||
subtitle,
|
|
||||||
series: cleanString(series),
|
|
||||||
publishYear: publishYear,
|
|
||||||
path: path,
|
|
||||||
fullPath: Path.join(abRootPath, path),
|
|
||||||
audioFiles: [],
|
audioFiles: [],
|
||||||
otherFiles: []
|
otherFiles: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileObj = {
|
var fileObj = {
|
||||||
filetype: getFileType(pathformat.ext),
|
filetype: getFileType(parsed.ext),
|
||||||
filename: pathformat.base,
|
filename: parsed.base,
|
||||||
path: relpath,
|
path: relpath,
|
||||||
fullPath: filepath,
|
fullPath: filepath,
|
||||||
ext: pathformat.ext
|
ext: parsed.ext
|
||||||
}
|
}
|
||||||
if (fileObj.filetype === 'audio') {
|
if (fileObj.filetype === 'audio') {
|
||||||
audiobooks[path].audioFiles.push(fileObj)
|
audiobooks[path].audioFiles.push(fileObj)
|
||||||
@ -100,4 +114,44 @@ async function getAllAudiobookFiles(abRootPath, serverSettings = {}) {
|
|||||||
})
|
})
|
||||||
return Object.values(audiobooks)
|
return Object.values(audiobooks)
|
||||||
}
|
}
|
||||||
module.exports.getAllAudiobookFiles = getAllAudiobookFiles
|
module.exports.getAllAudiobookFileData = getAllAudiobookFileData
|
||||||
|
|
||||||
|
|
||||||
|
async function getAudiobookFileData(abRootPath, audiobookPath, serverSettings = {}) {
|
||||||
|
var parseSubtitle = !!serverSettings.scannerParseSubtitle
|
||||||
|
|
||||||
|
var paths = await getPaths(audiobookPath)
|
||||||
|
var audiobook = null
|
||||||
|
|
||||||
|
paths.files.forEach((filepath) => {
|
||||||
|
var relpath = Path.normalize(filepath).replace(abRootPath, '').slice(1)
|
||||||
|
|
||||||
|
if (!audiobook) {
|
||||||
|
var audiobookData = getAudiobookDataFromFilepath(abRootPath, relpath, parseSubtitle)
|
||||||
|
if (!audiobookData) return
|
||||||
|
|
||||||
|
audiobook = {
|
||||||
|
...audiobookData,
|
||||||
|
audioFiles: [],
|
||||||
|
otherFiles: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var extname = Path.extname(filepath)
|
||||||
|
var basename = Path.basename(filepath)
|
||||||
|
var fileObj = {
|
||||||
|
filetype: getFileType(extname),
|
||||||
|
filename: basename,
|
||||||
|
path: relpath,
|
||||||
|
fullPath: filepath,
|
||||||
|
ext: extname
|
||||||
|
}
|
||||||
|
if (fileObj.filetype === 'audio') {
|
||||||
|
audiobook.audioFiles.push(fileObj)
|
||||||
|
} else {
|
||||||
|
audiobook.otherFiles.push(fileObj)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return audiobook
|
||||||
|
}
|
||||||
|
module.exports.getAudiobookFileData = getAudiobookFileData
|
Loading…
Reference in New Issue
Block a user