mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Add:Tools tab on library modal, api endpoint to remove all metadata files from library item folders
This commit is contained in:
		
							parent
							
								
									0d5792405f
								
							
						
					
					
						commit
						b4ce5342c0
					
				@ -12,9 +12,9 @@
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="px-2 md:px-4 w-full text-sm pt-2 md:pt-6 pb-20 rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
 | 
					    <div class="px-2 md:px-4 w-full text-sm pt-2 md:pt-6 pb-20 rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
 | 
				
			||||||
      <component v-if="libraryCopy && show" ref="tabComponent" :is="tabName" :is-new="!library" :library="libraryCopy" :processing.sync="processing" @update="updateLibrary" @close="show = false" />
 | 
					      <component v-if="libraryCopy && show" ref="tabComponent" :is="tabName" :is-new="!library" :library="libraryCopy" :library-id="libraryId" :processing.sync="processing" @update="updateLibrary" @close="show = false" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div class="absolute bottom-0 left-0 w-full px-4 py-4 border-t border-white border-opacity-10">
 | 
					      <div v-show="selectedTab !== 'tools'" class="absolute bottom-0 left-0 w-full px-4 py-4 border-t border-white border-opacity-10">
 | 
				
			||||||
        <div class="flex justify-end">
 | 
					        <div class="flex justify-end">
 | 
				
			||||||
          <ui-btn @click="submit">{{ buttonText }}</ui-btn>
 | 
					          <ui-btn @click="submit">{{ buttonText }}</ui-btn>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
@ -57,6 +57,9 @@ export default {
 | 
				
			|||||||
    mediaType() {
 | 
					    mediaType() {
 | 
				
			||||||
      return this.libraryCopy?.mediaType
 | 
					      return this.libraryCopy?.mediaType
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    libraryId() {
 | 
				
			||||||
 | 
					      return this.library?.id
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    tabs() {
 | 
					    tabs() {
 | 
				
			||||||
      return [
 | 
					      return [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
@ -78,6 +81,11 @@ export default {
 | 
				
			|||||||
          id: 'schedule',
 | 
					          id: 'schedule',
 | 
				
			||||||
          title: this.$strings.HeaderSchedule,
 | 
					          title: this.$strings.HeaderSchedule,
 | 
				
			||||||
          component: 'modals-libraries-schedule-scan'
 | 
					          component: 'modals-libraries-schedule-scan'
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          id: 'tools',
 | 
				
			||||||
 | 
					          title: this.$strings.HeaderTools,
 | 
				
			||||||
 | 
					          component: 'modals-libraries-library-tools'
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      ].filter((tab) => {
 | 
					      ].filter((tab) => {
 | 
				
			||||||
        return tab.id !== 'scanner' || this.mediaType === 'book'
 | 
					        return tab.id !== 'scanner' || this.mediaType === 'book'
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										70
									
								
								client/components/modals/libraries/LibraryTools.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								client/components/modals/libraries/LibraryTools.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,70 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="w-full h-full px-1 md:px-4 py-1 mb-4">
 | 
				
			||||||
 | 
					    <ui-btn class="mb-4" @click.stop="removeAllMetadataClick('json')">Remove all metadata.json files in library item folders</ui-btn>
 | 
				
			||||||
 | 
					    <ui-btn @click.stop="removeAllMetadataClick('abs')">Remove all metadata.abs files in library item folders</ui-btn>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  props: {
 | 
				
			||||||
 | 
					    library: {
 | 
				
			||||||
 | 
					      type: Object,
 | 
				
			||||||
 | 
					      default: () => null
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    libraryId: String,
 | 
				
			||||||
 | 
					    processing: Boolean
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  data() {
 | 
				
			||||||
 | 
					    return {}
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  computed: {
 | 
				
			||||||
 | 
					    librarySettings() {
 | 
				
			||||||
 | 
					      return this.library.settings || {}
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    mediaType() {
 | 
				
			||||||
 | 
					      return this.library.mediaType
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    isBookLibrary() {
 | 
				
			||||||
 | 
					      return this.mediaType === 'book'
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  methods: {
 | 
				
			||||||
 | 
					    removeAllMetadataClick(ext) {
 | 
				
			||||||
 | 
					      const payload = {
 | 
				
			||||||
 | 
					        message: `Are you sure you want to remove all metadata.${ext} files in your library item folders?`,
 | 
				
			||||||
 | 
					        persistent: true,
 | 
				
			||||||
 | 
					        callback: (confirmed) => {
 | 
				
			||||||
 | 
					          if (confirmed) {
 | 
				
			||||||
 | 
					            this.removeAllMetadataInLibrary(ext)
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        type: 'yesNo'
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      this.$store.commit('globals/setConfirmPrompt', payload)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    removeAllMetadataInLibrary(ext) {
 | 
				
			||||||
 | 
					      this.$emit('update:processing', true)
 | 
				
			||||||
 | 
					      this.$axios
 | 
				
			||||||
 | 
					        .$post(`/api/libraries/${this.libraryId}/remove-metadata?ext=${ext}`)
 | 
				
			||||||
 | 
					        .then((data) => {
 | 
				
			||||||
 | 
					          if (!data.found) {
 | 
				
			||||||
 | 
					            this.$toast.info(`No metadata.${ext} files were found in library`)
 | 
				
			||||||
 | 
					          } else if (!data.removed) {
 | 
				
			||||||
 | 
					            this.$toast.success(`No metadata.${ext} files removed`)
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            this.$toast.success(`Successfully removed ${data.removed} metadata.${ext} files`)
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .catch((error) => {
 | 
				
			||||||
 | 
					          console.error('Failed to remove metadata files', error)
 | 
				
			||||||
 | 
					          this.$toast.error('Failed to remove metadata files')
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .finally(() => {
 | 
				
			||||||
 | 
					          this.$emit('update:processing', false)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  mounted() {}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
@ -854,6 +854,56 @@ class LibraryController {
 | 
				
			|||||||
    res.send(opmlText)
 | 
					    res.send(opmlText)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Remove all metadata.json or metadata.abs files in library item folders
 | 
				
			||||||
 | 
					   * 
 | 
				
			||||||
 | 
					   * @param {import('express').Request} req 
 | 
				
			||||||
 | 
					   * @param {import('express').Response} res 
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  async removeAllMetadataFiles(req, res) {
 | 
				
			||||||
 | 
					    if (!req.user.isAdminOrUp) {
 | 
				
			||||||
 | 
					      Logger.error(`[LibraryController] Non-admin user attempted to remove all metadata files`, req.user)
 | 
				
			||||||
 | 
					      return res.sendStatus(403)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const fileExt = req.query.ext === 'abs' ? 'abs' : 'json'
 | 
				
			||||||
 | 
					    const metadataFilename = `metadata.${fileExt}`
 | 
				
			||||||
 | 
					    const libraryItemsWithMetadata = await Database.libraryItemModel.findAll({
 | 
				
			||||||
 | 
					      attributes: ['id', 'libraryFiles'],
 | 
				
			||||||
 | 
					      where: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          libraryId: req.library.id
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(libraryFiles) WHERE json_valid(libraryFiles) AND json_extract(json_each.value, "$.metadata.filename") = "${metadataFilename}")`), {
 | 
				
			||||||
 | 
					          [Sequelize.Op.gte]: 1
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    if (!libraryItemsWithMetadata.length) {
 | 
				
			||||||
 | 
					      Logger.info(`[LibraryController] No ${metadataFilename} files found to remove`)
 | 
				
			||||||
 | 
					      return res.json({
 | 
				
			||||||
 | 
					        found: 0
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Logger.info(`[LibraryController] Found ${libraryItemsWithMetadata.length} ${metadataFilename} files to remove`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let numRemoved = 0
 | 
				
			||||||
 | 
					    for (const libraryItem of libraryItemsWithMetadata) {
 | 
				
			||||||
 | 
					      const metadataFilepath = libraryItem.libraryFiles.find(lf => lf.metadata.filename === metadataFilename)?.metadata.path
 | 
				
			||||||
 | 
					      if (!metadataFilepath) continue
 | 
				
			||||||
 | 
					      Logger.debug(`[LibraryController] Removing file "${metadataFilepath}"`)
 | 
				
			||||||
 | 
					      if ((await fileUtils.removeFile(metadataFilepath))) {
 | 
				
			||||||
 | 
					        numRemoved++
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.json({
 | 
				
			||||||
 | 
					      found: libraryItemsWithMetadata.length,
 | 
				
			||||||
 | 
					      removed: numRemoved
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Middleware that is not using libraryItems from memory
 | 
					   * Middleware that is not using libraryItems from memory
 | 
				
			||||||
   * @param {import('express').Request} req 
 | 
					   * @param {import('express').Request} req 
 | 
				
			||||||
 | 
				
			|||||||
@ -84,6 +84,7 @@ class ApiRouter {
 | 
				
			|||||||
    this.router.get('/libraries/:id/recent-episodes', LibraryController.middleware.bind(this), LibraryController.getRecentEpisodes.bind(this))
 | 
					    this.router.get('/libraries/:id/recent-episodes', LibraryController.middleware.bind(this), LibraryController.getRecentEpisodes.bind(this))
 | 
				
			||||||
    this.router.get('/libraries/:id/opml', LibraryController.middleware.bind(this), LibraryController.getOPMLFile.bind(this))
 | 
					    this.router.get('/libraries/:id/opml', LibraryController.middleware.bind(this), LibraryController.getOPMLFile.bind(this))
 | 
				
			||||||
    this.router.post('/libraries/order', LibraryController.reorder.bind(this))
 | 
					    this.router.post('/libraries/order', LibraryController.reorder.bind(this))
 | 
				
			||||||
 | 
					    this.router.post('/libraries/:id/remove-metadata', LibraryController.middleware.bind(this), LibraryController.removeAllMetadataFiles.bind(this))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    //
 | 
					    //
 | 
				
			||||||
    // Item Routes
 | 
					    // Item Routes
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user