mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-11-03 19:07:00 -05:00 
			
		
		
		
	
		
			
				
	
	
		
			272 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			272 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
const Path = require('path')
 | 
						|
const EventEmitter = require('events')
 | 
						|
const Watcher = require('./libs/watcher/watcher')
 | 
						|
const Logger = require('./Logger')
 | 
						|
const LibraryScanner = require('./scanner/LibraryScanner')
 | 
						|
 | 
						|
const { filePathToPOSIX } = require('./utils/fileUtils')
 | 
						|
 | 
						|
/**
 | 
						|
 * @typedef PendingFileUpdate
 | 
						|
 * @property {string} path
 | 
						|
 * @property {string} relPath
 | 
						|
 * @property {string} folderId
 | 
						|
 * @property {string} type
 | 
						|
 */
 | 
						|
class FolderWatcher extends EventEmitter {
 | 
						|
  constructor() {
 | 
						|
    super()
 | 
						|
 | 
						|
    /** @type {{id:string, name:string, folders:import('./objects/Folder')[], paths:string[], watcher:Watcher[]}[]} */
 | 
						|
    this.libraryWatchers = []
 | 
						|
    /** @type {PendingFileUpdate[]} */
 | 
						|
    this.pendingFileUpdates = []
 | 
						|
    this.pendingDelay = 4000
 | 
						|
    this.pendingTimeout = null
 | 
						|
 | 
						|
    /** @type {string[]} */
 | 
						|
    this.ignoreDirs = []
 | 
						|
    /** @type {string[]} */
 | 
						|
    this.pendingDirsToRemoveFromIgnore = []
 | 
						|
 | 
						|
    this.disabled = false
 | 
						|
  }
 | 
						|
 | 
						|
  get pendingFilePaths() {
 | 
						|
    return this.pendingFileUpdates.map(f => f.path)
 | 
						|
  }
 | 
						|
 | 
						|
  buildLibraryWatcher(library) {
 | 
						|
    if (this.libraryWatchers.find(w => w.id === library.id)) {
 | 
						|
      Logger.warn('[Watcher] Already watching library', library.name)
 | 
						|
      return
 | 
						|
    }
 | 
						|
    Logger.info(`[Watcher] Initializing watcher for "${library.name}".`)
 | 
						|
 | 
						|
    const folderPaths = library.folderPaths
 | 
						|
    folderPaths.forEach((fp) => {
 | 
						|
      Logger.debug(`[Watcher] Init watcher for library folder path "${fp}"`)
 | 
						|
    })
 | 
						|
    const watcher = new Watcher(folderPaths, {
 | 
						|
      ignored: /(^|[\/\\])\../, // ignore dotfiles
 | 
						|
      renameDetection: true,
 | 
						|
      renameTimeout: 2000,
 | 
						|
      recursive: true,
 | 
						|
      ignoreInitial: true,
 | 
						|
      persistent: true
 | 
						|
    })
 | 
						|
    watcher
 | 
						|
      .on('add', (path) => {
 | 
						|
        this.onNewFile(library.id, path)
 | 
						|
      }).on('change', (path) => {
 | 
						|
        // This is triggered from metadata changes, not what we want
 | 
						|
        // this.onFileUpdated(path)
 | 
						|
      }).on('unlink', path => {
 | 
						|
        this.onFileRemoved(library.id, path)
 | 
						|
      }).on('rename', (path, pathNext) => {
 | 
						|
        this.onRename(library.id, path, pathNext)
 | 
						|
      }).on('error', (error) => {
 | 
						|
        Logger.error(`[Watcher] ${error}`)
 | 
						|
      }).on('ready', () => {
 | 
						|
        Logger.info(`[Watcher] "${library.name}" Ready`)
 | 
						|
      }).on('close', () => {
 | 
						|
        Logger.debug(`[Watcher] "${library.name}" Closed`)
 | 
						|
      })
 | 
						|
 | 
						|
    this.libraryWatchers.push({
 | 
						|
      id: library.id,
 | 
						|
      name: library.name,
 | 
						|
      folders: library.folders,
 | 
						|
      paths: library.folderPaths,
 | 
						|
      watcher
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  initWatcher(libraries) {
 | 
						|
    libraries.forEach((lib) => {
 | 
						|
      if (!lib.settings.disableWatcher) {
 | 
						|
        this.buildLibraryWatcher(lib)
 | 
						|
      }
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  addLibrary(library) {
 | 
						|
    if (this.disabled || library.settings.disableWatcher) return
 | 
						|
    this.buildLibraryWatcher(library)
 | 
						|
  }
 | 
						|
 | 
						|
  updateLibrary(library) {
 | 
						|
    if (this.disabled || library.settings.disableWatcher) return
 | 
						|
    var libwatcher = this.libraryWatchers.find(lib => lib.id === library.id)
 | 
						|
    if (libwatcher) {
 | 
						|
      libwatcher.name = library.name
 | 
						|
 | 
						|
      // If any folder paths were added or removed then re-init watcher
 | 
						|
      var pathsToAdd = library.folderPaths.filter(path => !libwatcher.paths.includes(path))
 | 
						|
      var pathsRemoved = libwatcher.paths.filter(path => !library.folderPaths.includes(path))
 | 
						|
      if (pathsToAdd.length || pathsRemoved.length) {
 | 
						|
        Logger.info(`[Watcher] Re-Initializing watcher for "${library.name}".`)
 | 
						|
 | 
						|
        libwatcher.watcher.close()
 | 
						|
        this.libraryWatchers = this.libraryWatchers.filter(lw => lw.id !== libwatcher.id)
 | 
						|
        this.buildLibraryWatcher(library)
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  removeLibrary(library) {
 | 
						|
    if (this.disabled) return
 | 
						|
    var libwatcher = this.libraryWatchers.find(lib => lib.id === library.id)
 | 
						|
    if (libwatcher) {
 | 
						|
      Logger.info(`[Watcher] Removed watcher for "${library.name}"`)
 | 
						|
      libwatcher.watcher.close()
 | 
						|
      this.libraryWatchers = this.libraryWatchers.filter(lib => lib.id !== library.id)
 | 
						|
    } else {
 | 
						|
      Logger.error(`[Watcher] Library watcher not found for "${library.name}"`)
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  close() {
 | 
						|
    return this.libraryWatchers.map(lib => lib.watcher.close())
 | 
						|
  }
 | 
						|
 | 
						|
  onNewFile(libraryId, path) {
 | 
						|
    if (this.checkShouldIgnorePath(path)) {
 | 
						|
      return
 | 
						|
    }
 | 
						|
    Logger.debug('[Watcher] File Added', path)
 | 
						|
    this.addFileUpdate(libraryId, path, 'added')
 | 
						|
  }
 | 
						|
 | 
						|
  onFileRemoved(libraryId, path) {
 | 
						|
    if (this.checkShouldIgnorePath(path)) {
 | 
						|
      return
 | 
						|
    }
 | 
						|
    Logger.debug('[Watcher] File Removed', path)
 | 
						|
    this.addFileUpdate(libraryId, path, 'deleted')
 | 
						|
  }
 | 
						|
 | 
						|
  onFileUpdated(path) {
 | 
						|
    Logger.debug('[Watcher] Updated File', path)
 | 
						|
  }
 | 
						|
 | 
						|
  onRename(libraryId, pathFrom, pathTo) {
 | 
						|
    if (this.checkShouldIgnorePath(pathTo)) {
 | 
						|
      return
 | 
						|
    }
 | 
						|
    Logger.debug(`[Watcher] Rename ${pathFrom} => ${pathTo}`)
 | 
						|
    this.addFileUpdate(libraryId, pathTo, 'renamed')
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * File update detected from watcher
 | 
						|
   * @param {string} libraryId 
 | 
						|
   * @param {string} path 
 | 
						|
   * @param {string} type 
 | 
						|
   */
 | 
						|
  addFileUpdate(libraryId, path, type) {
 | 
						|
    path = filePathToPOSIX(path)
 | 
						|
    if (this.pendingFilePaths.includes(path)) return
 | 
						|
 | 
						|
    // Get file library
 | 
						|
    const libwatcher = this.libraryWatchers.find(lw => lw.id === libraryId)
 | 
						|
    if (!libwatcher) {
 | 
						|
      Logger.error(`[Watcher] Invalid library id from watcher ${libraryId}`)
 | 
						|
      return
 | 
						|
    }
 | 
						|
 | 
						|
    // Get file folder
 | 
						|
    const folder = libwatcher.folders.find(fold => path.startsWith(filePathToPOSIX(fold.fullPath)))
 | 
						|
    if (!folder) {
 | 
						|
      Logger.error(`[Watcher] New file folder not found in library "${libwatcher.name}" with path "${path}"`)
 | 
						|
      return
 | 
						|
    }
 | 
						|
 | 
						|
    const folderFullPath = filePathToPOSIX(folder.fullPath)
 | 
						|
 | 
						|
    const relPath = path.replace(folderFullPath, '')
 | 
						|
 | 
						|
    if (Path.extname(relPath).toLowerCase() === '.part') {
 | 
						|
      Logger.debug(`[Watcher] Ignoring .part file "${relPath}"`)
 | 
						|
      return
 | 
						|
    }
 | 
						|
 | 
						|
    // Ignore files/folders starting with "."
 | 
						|
    const hasDotPath = relPath.split('/').find(p => p.startsWith('.'))
 | 
						|
    if (hasDotPath) {
 | 
						|
      Logger.debug(`[Watcher] Ignoring dot path "${relPath}" | Piece "${hasDotPath}"`)
 | 
						|
      return
 | 
						|
    }
 | 
						|
 | 
						|
    Logger.debug(`[Watcher] Modified file in library "${libwatcher.name}" and folder "${folder.id}" with relPath "${relPath}"`)
 | 
						|
 | 
						|
    this.pendingFileUpdates.push({
 | 
						|
      path,
 | 
						|
      relPath,
 | 
						|
      folderId: folder.id,
 | 
						|
      libraryId,
 | 
						|
      type
 | 
						|
    })
 | 
						|
 | 
						|
    // Notify server of update after "pendingDelay"
 | 
						|
    clearTimeout(this.pendingTimeout)
 | 
						|
    this.pendingTimeout = setTimeout(() => {
 | 
						|
      // this.emit('files', this.pendingFileUpdates)
 | 
						|
      LibraryScanner.scanFilesChanged(this.pendingFileUpdates)
 | 
						|
      this.pendingFileUpdates = []
 | 
						|
    }, this.pendingDelay)
 | 
						|
  }
 | 
						|
 | 
						|
  checkShouldIgnorePath(path) {
 | 
						|
    return !!this.ignoreDirs.find(dirpath => {
 | 
						|
      return filePathToPOSIX(path).startsWith(dirpath)
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Convert to POSIX and remove trailing slash
 | 
						|
   * @param {string} path 
 | 
						|
   * @returns {string}
 | 
						|
   */
 | 
						|
  cleanDirPath(path) {
 | 
						|
    path = filePathToPOSIX(path)
 | 
						|
    if (path.endsWith('/')) path = path.slice(0, -1)
 | 
						|
    return path
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Ignore this directory if files are picked up by watcher
 | 
						|
   * @param {string} path 
 | 
						|
   */
 | 
						|
  addIgnoreDir(path) {
 | 
						|
    path = this.cleanDirPath(path)
 | 
						|
    if (this.ignoreDirs.includes(path)) return
 | 
						|
    this.pendingDirsToRemoveFromIgnore = this.pendingDirsToRemoveFromIgnore.filter(p => p !== path)
 | 
						|
    Logger.debug(`[Watcher] Ignoring directory "${path}"`)
 | 
						|
    this.ignoreDirs.push(path)
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * When downloading a podcast episode we dont want the scanner triggering for that podcast
 | 
						|
   * when the episode finishes the watcher may have a delayed response so a timeout is added
 | 
						|
   * to prevent the watcher from picking up the episode
 | 
						|
   * 
 | 
						|
   * @param {string} path 
 | 
						|
   */
 | 
						|
  removeIgnoreDir(path) {
 | 
						|
    path = this.cleanDirPath(path)
 | 
						|
    if (!this.ignoreDirs.includes(path) || this.pendingDirsToRemoveFromIgnore.includes(path)) return
 | 
						|
 | 
						|
    // Add a 5 second delay before removing the ignore from this dir
 | 
						|
    this.pendingDirsToRemoveFromIgnore.push(path)
 | 
						|
    setTimeout(() => {
 | 
						|
      if (this.pendingDirsToRemoveFromIgnore.includes(path)) {
 | 
						|
        this.pendingDirsToRemoveFromIgnore = this.pendingDirsToRemoveFromIgnore.filter(p => p !== path)
 | 
						|
        Logger.debug(`[Watcher] No longer ignoring directory "${path}"`)
 | 
						|
        this.ignoreDirs = this.ignoreDirs.filter(p => p !== path)
 | 
						|
      }
 | 
						|
    }, 5000)
 | 
						|
 | 
						|
  }
 | 
						|
}
 | 
						|
module.exports = FolderWatcher |