mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Update:Create library endpoint to create using new model, adding additional validation
This commit is contained in:
		
							parent
							
								
									1c0d6e9c67
								
							
						
					
					
						commit
						8774e6be71
					
				@ -384,11 +384,6 @@ class Database {
 | 
			
		||||
    return Promise.all(oldBooks.map((oldBook) => this.models.book.saveFromOld(oldBook)))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  createLibrary(oldLibrary) {
 | 
			
		||||
    if (!this.sequelize) return false
 | 
			
		||||
    return this.models.library.createFromOld(oldLibrary)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateLibrary(oldLibrary) {
 | 
			
		||||
    if (!this.sequelize) return false
 | 
			
		||||
    return this.models.library.updateFromOld(oldLibrary)
 | 
			
		||||
 | 
			
		||||
@ -41,58 +41,126 @@ class LibraryController {
 | 
			
		||||
   * @param {Response} res
 | 
			
		||||
   */
 | 
			
		||||
  async create(req, res) {
 | 
			
		||||
    const newLibraryPayload = {
 | 
			
		||||
      ...req.body
 | 
			
		||||
    if (!req.user.isAdminOrUp) {
 | 
			
		||||
      Logger.error(`[LibraryController] Non-admin user "${req.user.username}" attempted to create library`)
 | 
			
		||||
      return res.sendStatus(403)
 | 
			
		||||
    }
 | 
			
		||||
    if (!newLibraryPayload.name || !newLibraryPayload.folders || !newLibraryPayload.folders.length) {
 | 
			
		||||
      return res.status(500).send('Invalid request')
 | 
			
		||||
 | 
			
		||||
    // Validation
 | 
			
		||||
    if (!req.body.name || typeof req.body.name !== 'string') {
 | 
			
		||||
      return res.status(400).send('Invalid request. Name must be a string')
 | 
			
		||||
    }
 | 
			
		||||
    if (
 | 
			
		||||
      !Array.isArray(req.body.folders) ||
 | 
			
		||||
      req.body.folders.some((f) => {
 | 
			
		||||
        // Old model uses fullPath and new model will use path. Support both for now
 | 
			
		||||
        const path = f?.fullPath || f?.path
 | 
			
		||||
        return !path || typeof path !== 'string'
 | 
			
		||||
      })
 | 
			
		||||
    ) {
 | 
			
		||||
      return res.status(400).send('Invalid request. Folders must be a non-empty array of objects with path string')
 | 
			
		||||
    }
 | 
			
		||||
    const optionalStringFields = ['mediaType', 'icon', 'provider']
 | 
			
		||||
    for (const field of optionalStringFields) {
 | 
			
		||||
      if (req.body[field] && typeof req.body[field] !== 'string') {
 | 
			
		||||
        return res.status(400).send(`Invalid request. ${field} must be a string`)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (req.body.settings && (typeof req.body.settings !== 'object' || Array.isArray(req.body.settings))) {
 | 
			
		||||
      return res.status(400).send('Invalid request. Settings must be an object')
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const mediaType = req.body.mediaType || 'book'
 | 
			
		||||
    const newLibraryPayload = {
 | 
			
		||||
      name: req.body.name,
 | 
			
		||||
      provider: req.body.provider || 'google',
 | 
			
		||||
      mediaType,
 | 
			
		||||
      icon: req.body.icon || 'database',
 | 
			
		||||
      settings: Database.libraryModel.getDefaultLibrarySettingsForMediaType(mediaType)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Validate settings
 | 
			
		||||
    if (req.body.settings) {
 | 
			
		||||
      for (const key in req.body.settings) {
 | 
			
		||||
        if (newLibraryPayload.settings[key] !== undefined) {
 | 
			
		||||
          if (key === 'metadataPrecedence') {
 | 
			
		||||
            if (!Array.isArray(req.body.settings[key])) {
 | 
			
		||||
              return res.status(400).send('Invalid request. Settings "metadataPrecedence" must be an array')
 | 
			
		||||
            }
 | 
			
		||||
            newLibraryPayload.settings[key] = [...req.body.settings[key]]
 | 
			
		||||
          } else if (key === 'autoScanCronExpression' || key === 'podcastSearchRegion') {
 | 
			
		||||
            if (!req.body.settings[key]) continue
 | 
			
		||||
            if (typeof req.body.settings[key] !== 'string') {
 | 
			
		||||
              return res.status(400).send(`Invalid request. Settings "${key}" must be a string`)
 | 
			
		||||
            }
 | 
			
		||||
            newLibraryPayload.settings[key] = req.body.settings[key]
 | 
			
		||||
          } else {
 | 
			
		||||
            if (typeof req.body.settings[key] !== typeof newLibraryPayload.settings[key]) {
 | 
			
		||||
              return res.status(400).send(`Invalid request. Setting "${key}" must be of type ${typeof newLibraryPayload.settings[key]}`)
 | 
			
		||||
            }
 | 
			
		||||
            newLibraryPayload.settings[key] = req.body.settings[key]
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Validate that the custom provider exists if given any
 | 
			
		||||
    if (newLibraryPayload.provider?.startsWith('custom-')) {
 | 
			
		||||
    if (newLibraryPayload.provider.startsWith('custom-')) {
 | 
			
		||||
      if (!(await Database.customMetadataProviderModel.checkExistsBySlug(newLibraryPayload.provider))) {
 | 
			
		||||
        Logger.error(`[LibraryController] Custom metadata provider "${newLibraryPayload.provider}" does not exist`)
 | 
			
		||||
        return res.status(400).send('Custom metadata provider does not exist')
 | 
			
		||||
        return res.status(400).send('Invalid request. Custom metadata provider does not exist')
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Validate folder paths exist or can be created & resolve rel paths
 | 
			
		||||
    //   returns 400 if a folder fails to access
 | 
			
		||||
    newLibraryPayload.folders = newLibraryPayload.folders.map((f) => {
 | 
			
		||||
      f.fullPath = fileUtils.filePathToPOSIX(Path.resolve(f.fullPath))
 | 
			
		||||
    newLibraryPayload.libraryFolders = req.body.folders.map((f) => {
 | 
			
		||||
      const fpath = f.fullPath || f.path
 | 
			
		||||
      f.path = fileUtils.filePathToPOSIX(Path.resolve(fpath))
 | 
			
		||||
      return f
 | 
			
		||||
    })
 | 
			
		||||
    for (const folder of newLibraryPayload.folders) {
 | 
			
		||||
    for (const folder of newLibraryPayload.libraryFolders) {
 | 
			
		||||
      try {
 | 
			
		||||
        const direxists = await fs.pathExists(folder.fullPath)
 | 
			
		||||
        if (!direxists) {
 | 
			
		||||
          // If folder does not exist try to make it and set file permissions/owner
 | 
			
		||||
          await fs.mkdir(folder.fullPath)
 | 
			
		||||
        }
 | 
			
		||||
        // Create folder if it doesn't exist
 | 
			
		||||
        await fs.ensureDir(folder.path)
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        Logger.error(`[LibraryController] Failed to ensure folder dir "${folder.fullPath}"`, error)
 | 
			
		||||
        return res.status(400).send(`Invalid folder directory "${folder.fullPath}"`)
 | 
			
		||||
        Logger.error(`[LibraryController] Failed to ensure folder dir "${folder.path}"`, error)
 | 
			
		||||
        return res.status(400).send(`Invalid request. Invalid folder directory "${folder.path}"`)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const library = new Library()
 | 
			
		||||
 | 
			
		||||
    // Set display order
 | 
			
		||||
    let currentLargestDisplayOrder = await Database.libraryModel.getMaxDisplayOrder()
 | 
			
		||||
    if (isNaN(currentLargestDisplayOrder)) currentLargestDisplayOrder = 0
 | 
			
		||||
    newLibraryPayload.displayOrder = currentLargestDisplayOrder + 1
 | 
			
		||||
    library.setData(newLibraryPayload)
 | 
			
		||||
    await Database.createLibrary(library)
 | 
			
		||||
 | 
			
		||||
    // Create library with libraryFolders
 | 
			
		||||
    const library = await Database.libraryModel
 | 
			
		||||
      .create(newLibraryPayload, {
 | 
			
		||||
        include: Database.libraryFolderModel
 | 
			
		||||
      })
 | 
			
		||||
      .catch((error) => {
 | 
			
		||||
        Logger.error(`[LibraryController] Failed to create library "${newLibraryPayload.name}"`, error)
 | 
			
		||||
      })
 | 
			
		||||
    if (!library) {
 | 
			
		||||
      return res.status(500).send('Failed to create library')
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    library.libraryFolders = await library.getLibraryFolders()
 | 
			
		||||
 | 
			
		||||
    // TODO: Migrate to new library model
 | 
			
		||||
    const oldLibrary = Database.libraryModel.getOldLibrary(library)
 | 
			
		||||
 | 
			
		||||
    // Only emit to users with access to library
 | 
			
		||||
    const userFilter = (user) => {
 | 
			
		||||
      return user.checkCanAccessLibrary?.(library.id)
 | 
			
		||||
      return user.checkCanAccessLibrary?.(oldLibrary.id)
 | 
			
		||||
    }
 | 
			
		||||
    SocketAuthority.emitter('library_added', library.toJSON(), userFilter)
 | 
			
		||||
    SocketAuthority.emitter('library_added', oldLibrary.toJSON(), userFilter)
 | 
			
		||||
 | 
			
		||||
    // Add library watcher
 | 
			
		||||
    this.watcher.addLibrary(library)
 | 
			
		||||
    this.watcher.addLibrary(oldLibrary)
 | 
			
		||||
 | 
			
		||||
    res.json(library)
 | 
			
		||||
    res.json(oldLibrary)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async findAll(req, res) {
 | 
			
		||||
 | 
			
		||||
@ -45,6 +45,35 @@ class Library extends Model {
 | 
			
		||||
    this.updatedAt
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   *
 | 
			
		||||
   * @param {string} mediaType
 | 
			
		||||
   * @returns
 | 
			
		||||
   */
 | 
			
		||||
  static getDefaultLibrarySettingsForMediaType(mediaType) {
 | 
			
		||||
    if (mediaType === 'podcast') {
 | 
			
		||||
      return {
 | 
			
		||||
        coverAspectRatio: 1, // Square
 | 
			
		||||
        disableWatcher: false,
 | 
			
		||||
        autoScanCronExpression: null,
 | 
			
		||||
        podcastSearchRegion: 'us'
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      return {
 | 
			
		||||
        coverAspectRatio: 1, // Square
 | 
			
		||||
        disableWatcher: false,
 | 
			
		||||
        autoScanCronExpression: null,
 | 
			
		||||
        skipMatchingMediaWithAsin: false,
 | 
			
		||||
        skipMatchingMediaWithIsbn: false,
 | 
			
		||||
        audiobooksOnly: false,
 | 
			
		||||
        epubsAllowScriptedContent: false,
 | 
			
		||||
        hideSingleBookSeries: false,
 | 
			
		||||
        onlyShowLaterBooksInContinueSeries: false,
 | 
			
		||||
        metadataPrecedence: ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata']
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get all old libraries
 | 
			
		||||
   * @returns {Promise<oldLibrary[]>}
 | 
			
		||||
@ -89,28 +118,6 @@ class Library extends Model {
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {object} oldLibrary
 | 
			
		||||
   * @returns {Library|null}
 | 
			
		||||
   */
 | 
			
		||||
  static async createFromOld(oldLibrary) {
 | 
			
		||||
    const library = this.getFromOld(oldLibrary)
 | 
			
		||||
 | 
			
		||||
    library.libraryFolders = oldLibrary.folders.map((folder) => {
 | 
			
		||||
      return {
 | 
			
		||||
        id: folder.id,
 | 
			
		||||
        path: folder.fullPath
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    return this.create(library, {
 | 
			
		||||
      include: this.sequelize.models.libraryFolder
 | 
			
		||||
    }).catch((error) => {
 | 
			
		||||
      Logger.error(`[Library] Failed to create library ${library.id}`, error)
 | 
			
		||||
      return null
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Update library and library folders
 | 
			
		||||
   * @param {object} oldLibrary
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
const uuidv4 = require("uuid").v4
 | 
			
		||||
const uuidv4 = require('uuid').v4
 | 
			
		||||
const Folder = require('./Folder')
 | 
			
		||||
const LibrarySettings = require('./settings/LibrarySettings')
 | 
			
		||||
const { filePathToPOSIX } = require('../utils/fileUtils')
 | 
			
		||||
@ -29,7 +29,7 @@ class Library {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get folderPaths() {
 | 
			
		||||
    return this.folders.map(f => f.fullPath)
 | 
			
		||||
    return this.folders.map((f) => f.fullPath)
 | 
			
		||||
  }
 | 
			
		||||
  get isPodcast() {
 | 
			
		||||
    return this.mediaType === 'podcast'
 | 
			
		||||
@ -45,14 +45,15 @@ class Library {
 | 
			
		||||
    this.id = library.id
 | 
			
		||||
    this.oldLibraryId = library.oldLibraryId
 | 
			
		||||
    this.name = library.name
 | 
			
		||||
    this.folders = (library.folders || []).map(f => new Folder(f))
 | 
			
		||||
    this.folders = (library.folders || []).map((f) => new Folder(f))
 | 
			
		||||
    this.displayOrder = library.displayOrder || 1
 | 
			
		||||
    this.icon = library.icon || 'database'
 | 
			
		||||
    this.mediaType = library.mediaType
 | 
			
		||||
    this.provider = library.provider || 'google'
 | 
			
		||||
 | 
			
		||||
    this.settings = new LibrarySettings(library.settings)
 | 
			
		||||
    if (library.settings === undefined) { // LibrarySettings added in v2, migrate settings
 | 
			
		||||
    if (library.settings === undefined) {
 | 
			
		||||
      // LibrarySettings added in v2, migrate settings
 | 
			
		||||
      this.settings.disableWatcher = !!library.disableWatcher
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -85,7 +86,7 @@ class Library {
 | 
			
		||||
      id: this.id,
 | 
			
		||||
      oldLibraryId: this.oldLibraryId,
 | 
			
		||||
      name: this.name,
 | 
			
		||||
      folders: (this.folders || []).map(f => f.toJSON()),
 | 
			
		||||
      folders: (this.folders || []).map((f) => f.toJSON()),
 | 
			
		||||
      displayOrder: this.displayOrder,
 | 
			
		||||
      icon: this.icon,
 | 
			
		||||
      mediaType: this.mediaType,
 | 
			
		||||
@ -98,32 +99,6 @@ class Library {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setData(data) {
 | 
			
		||||
    this.id = data.id || uuidv4()
 | 
			
		||||
    this.name = data.name
 | 
			
		||||
    if (data.folder) {
 | 
			
		||||
      this.folders = [
 | 
			
		||||
        new Folder(data.folder)
 | 
			
		||||
      ]
 | 
			
		||||
    } else if (data.folders) {
 | 
			
		||||
      this.folders = data.folders.map(folder => {
 | 
			
		||||
        var newFolder = new Folder()
 | 
			
		||||
        newFolder.setData({
 | 
			
		||||
          fullPath: folder.fullPath,
 | 
			
		||||
          libraryId: this.id
 | 
			
		||||
        })
 | 
			
		||||
        return newFolder
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
    this.displayOrder = data.displayOrder || 1
 | 
			
		||||
    this.icon = data.icon || 'database'
 | 
			
		||||
    this.mediaType = data.mediaType || 'book'
 | 
			
		||||
    this.provider = data.provider || 'google'
 | 
			
		||||
    this.settings = new LibrarySettings(data.settings)
 | 
			
		||||
    this.createdAt = Date.now()
 | 
			
		||||
    this.lastUpdate = Date.now()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  update(payload) {
 | 
			
		||||
    let hasUpdates = false
 | 
			
		||||
 | 
			
		||||
@ -144,12 +119,12 @@ class Library {
 | 
			
		||||
      hasUpdates = true
 | 
			
		||||
    }
 | 
			
		||||
    if (payload.folders) {
 | 
			
		||||
      const newFolders = payload.folders.filter(f => !f.id)
 | 
			
		||||
      const removedFolders = this.folders.filter(f => !payload.folders.some(_f => _f.id === f.id))
 | 
			
		||||
      const newFolders = payload.folders.filter((f) => !f.id)
 | 
			
		||||
      const removedFolders = this.folders.filter((f) => !payload.folders.some((_f) => _f.id === f.id))
 | 
			
		||||
 | 
			
		||||
      if (removedFolders.length) {
 | 
			
		||||
        const removedFolderIds = removedFolders.map(f => f.id)
 | 
			
		||||
        this.folders = this.folders.filter(f => !removedFolderIds.includes(f.id))
 | 
			
		||||
        const removedFolderIds = removedFolders.map((f) => f.id)
 | 
			
		||||
        this.folders = this.folders.filter((f) => !removedFolderIds.includes(f.id))
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (newFolders.length) {
 | 
			
		||||
@ -173,11 +148,11 @@ class Library {
 | 
			
		||||
 | 
			
		||||
  checkFullPathInLibrary(fullPath) {
 | 
			
		||||
    fullPath = filePathToPOSIX(fullPath)
 | 
			
		||||
    return this.folders.find(folder => fullPath.startsWith(filePathToPOSIX(folder.fullPath)))
 | 
			
		||||
    return this.folders.find((folder) => fullPath.startsWith(filePathToPOSIX(folder.fullPath)))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getFolderById(id) {
 | 
			
		||||
    return this.folders.find(folder => folder.id === id)
 | 
			
		||||
    return this.folders.find((folder) => folder.id === id)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
module.exports = Library
 | 
			
		||||
module.exports = Library
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user