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))) |     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) { |   updateLibrary(oldLibrary) { | ||||||
|     if (!this.sequelize) return false |     if (!this.sequelize) return false | ||||||
|     return this.models.library.updateFromOld(oldLibrary) |     return this.models.library.updateFromOld(oldLibrary) | ||||||
|  | |||||||
| @ -41,58 +41,126 @@ class LibraryController { | |||||||
|    * @param {Response} res |    * @param {Response} res | ||||||
|    */ |    */ | ||||||
|   async create(req, res) { |   async create(req, res) { | ||||||
|     const newLibraryPayload = { |     if (!req.user.isAdminOrUp) { | ||||||
|       ...req.body |       Logger.error(`[LibraryController] Non-admin user "${req.user.username}" attempted to create library`) | ||||||
|  |       return res.sendStatus(403) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // 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] | ||||||
|  |           } | ||||||
|  |         } | ||||||
|       } |       } | ||||||
|     if (!newLibraryPayload.name || !newLibraryPayload.folders || !newLibraryPayload.folders.length) { |  | ||||||
|       return res.status(500).send('Invalid request') |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Validate that the custom provider exists if given any
 |     // 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))) { |       if (!(await Database.customMetadataProviderModel.checkExistsBySlug(newLibraryPayload.provider))) { | ||||||
|         Logger.error(`[LibraryController] Custom metadata provider "${newLibraryPayload.provider}" does not exist`) |         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
 |     // Validate folder paths exist or can be created & resolve rel paths
 | ||||||
|     //   returns 400 if a folder fails to access
 |     //   returns 400 if a folder fails to access
 | ||||||
|     newLibraryPayload.folders = newLibraryPayload.folders.map((f) => { |     newLibraryPayload.libraryFolders = req.body.folders.map((f) => { | ||||||
|       f.fullPath = fileUtils.filePathToPOSIX(Path.resolve(f.fullPath)) |       const fpath = f.fullPath || f.path | ||||||
|  |       f.path = fileUtils.filePathToPOSIX(Path.resolve(fpath)) | ||||||
|       return f |       return f | ||||||
|     }) |     }) | ||||||
|     for (const folder of newLibraryPayload.folders) { |     for (const folder of newLibraryPayload.libraryFolders) { | ||||||
|       try { |       try { | ||||||
|         const direxists = await fs.pathExists(folder.fullPath) |         // Create folder if it doesn't exist
 | ||||||
|         if (!direxists) { |         await fs.ensureDir(folder.path) | ||||||
|           // If folder does not exist try to make it and set file permissions/owner
 |  | ||||||
|           await fs.mkdir(folder.fullPath) |  | ||||||
|         } |  | ||||||
|       } catch (error) { |       } catch (error) { | ||||||
|         Logger.error(`[LibraryController] Failed to ensure folder dir "${folder.fullPath}"`, error) |         Logger.error(`[LibraryController] Failed to ensure folder dir "${folder.path}"`, error) | ||||||
|         return res.status(400).send(`Invalid folder directory "${folder.fullPath}"`) |         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() |     let currentLargestDisplayOrder = await Database.libraryModel.getMaxDisplayOrder() | ||||||
|     if (isNaN(currentLargestDisplayOrder)) currentLargestDisplayOrder = 0 |     if (isNaN(currentLargestDisplayOrder)) currentLargestDisplayOrder = 0 | ||||||
|     newLibraryPayload.displayOrder = currentLargestDisplayOrder + 1 |     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
 |     // Only emit to users with access to library
 | ||||||
|     const userFilter = (user) => { |     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
 |     // Add library watcher
 | ||||||
|     this.watcher.addLibrary(library) |     this.watcher.addLibrary(oldLibrary) | ||||||
| 
 | 
 | ||||||
|     res.json(library) |     res.json(oldLibrary) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async findAll(req, res) { |   async findAll(req, res) { | ||||||
|  | |||||||
| @ -45,6 +45,35 @@ class Library extends Model { | |||||||
|     this.updatedAt |     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 |    * Get all old libraries | ||||||
|    * @returns {Promise<oldLibrary[]>} |    * @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 |    * Update library and library folders | ||||||
|    * @param {object} oldLibrary |    * @param {object} oldLibrary | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| const uuidv4 = require("uuid").v4 | const uuidv4 = require('uuid').v4 | ||||||
| const Folder = require('./Folder') | const Folder = require('./Folder') | ||||||
| const LibrarySettings = require('./settings/LibrarySettings') | const LibrarySettings = require('./settings/LibrarySettings') | ||||||
| const { filePathToPOSIX } = require('../utils/fileUtils') | const { filePathToPOSIX } = require('../utils/fileUtils') | ||||||
| @ -29,7 +29,7 @@ class Library { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   get folderPaths() { |   get folderPaths() { | ||||||
|     return this.folders.map(f => f.fullPath) |     return this.folders.map((f) => f.fullPath) | ||||||
|   } |   } | ||||||
|   get isPodcast() { |   get isPodcast() { | ||||||
|     return this.mediaType === 'podcast' |     return this.mediaType === 'podcast' | ||||||
| @ -45,14 +45,15 @@ class Library { | |||||||
|     this.id = library.id |     this.id = library.id | ||||||
|     this.oldLibraryId = library.oldLibraryId |     this.oldLibraryId = library.oldLibraryId | ||||||
|     this.name = library.name |     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.displayOrder = library.displayOrder || 1 | ||||||
|     this.icon = library.icon || 'database' |     this.icon = library.icon || 'database' | ||||||
|     this.mediaType = library.mediaType |     this.mediaType = library.mediaType | ||||||
|     this.provider = library.provider || 'google' |     this.provider = library.provider || 'google' | ||||||
| 
 | 
 | ||||||
|     this.settings = new LibrarySettings(library.settings) |     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 |       this.settings.disableWatcher = !!library.disableWatcher | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -85,7 +86,7 @@ class Library { | |||||||
|       id: this.id, |       id: this.id, | ||||||
|       oldLibraryId: this.oldLibraryId, |       oldLibraryId: this.oldLibraryId, | ||||||
|       name: this.name, |       name: this.name, | ||||||
|       folders: (this.folders || []).map(f => f.toJSON()), |       folders: (this.folders || []).map((f) => f.toJSON()), | ||||||
|       displayOrder: this.displayOrder, |       displayOrder: this.displayOrder, | ||||||
|       icon: this.icon, |       icon: this.icon, | ||||||
|       mediaType: this.mediaType, |       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) { |   update(payload) { | ||||||
|     let hasUpdates = false |     let hasUpdates = false | ||||||
| 
 | 
 | ||||||
| @ -144,12 +119,12 @@ class Library { | |||||||
|       hasUpdates = true |       hasUpdates = true | ||||||
|     } |     } | ||||||
|     if (payload.folders) { |     if (payload.folders) { | ||||||
|       const newFolders = payload.folders.filter(f => !f.id) |       const newFolders = payload.folders.filter((f) => !f.id) | ||||||
|       const removedFolders = this.folders.filter(f => !payload.folders.some(_f => _f.id === f.id)) |       const removedFolders = this.folders.filter((f) => !payload.folders.some((_f) => _f.id === f.id)) | ||||||
| 
 | 
 | ||||||
|       if (removedFolders.length) { |       if (removedFolders.length) { | ||||||
|         const removedFolderIds = removedFolders.map(f => f.id) |         const removedFolderIds = removedFolders.map((f) => f.id) | ||||||
|         this.folders = this.folders.filter(f => !removedFolderIds.includes(f.id)) |         this.folders = this.folders.filter((f) => !removedFolderIds.includes(f.id)) | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       if (newFolders.length) { |       if (newFolders.length) { | ||||||
| @ -173,11 +148,11 @@ class Library { | |||||||
| 
 | 
 | ||||||
|   checkFullPathInLibrary(fullPath) { |   checkFullPathInLibrary(fullPath) { | ||||||
|     fullPath = filePathToPOSIX(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) { |   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