diff --git a/client/components/cards/ItemTaskRunningCard.vue b/client/components/cards/ItemTaskRunningCard.vue index c9de1a87..d284c505 100644 --- a/client/components/cards/ItemTaskRunningCard.vue +++ b/client/components/cards/ItemTaskRunningCard.vue @@ -12,7 +12,7 @@

{{ failedMessage }}

Canceling...

- {{ this.$strings.ButtonCancel }} + {{ this.$strings.ButtonCancel }} @@ -81,6 +81,9 @@ export default { } return '' + }, + isLibraryScan() { + return this.action === 'library-scan' || this.action === 'library-match-all' } }, methods: { diff --git a/client/components/modals/item/tabs/Details.vue b/client/components/modals/item/tabs/Details.vue index 14fe68a7..62f08c92 100644 --- a/client/components/modals/item/tabs/Details.vue +++ b/client/components/modals/item/tabs/Details.vue @@ -11,8 +11,8 @@ {{ $strings.ButtonQuickMatch }} - - {{ $strings.ButtonReScan }} + + {{ $strings.ButtonReScan }}
@@ -80,9 +80,9 @@ export default { libraryProvider() { return this.$store.getters['libraries/getLibraryProvider'](this.libraryId) || 'google' }, - libraryScan() { + isLibraryScanning() { if (!this.libraryId) return null - return this.$store.getters['scanners/getLibraryScan'](this.libraryId) + return !!this.$store.getters['tasks/getRunningLibraryScanTask'](this.libraryId) } }, methods: { diff --git a/client/components/tables/library/LibrariesTable.vue b/client/components/tables/library/LibrariesTable.vue index 598b12b7..faf8d69d 100644 --- a/client/components/tables/library/LibrariesTable.vue +++ b/client/components/tables/library/LibrariesTable.vue @@ -42,13 +42,10 @@ export default { return this.$store.getters['libraries/getCurrentLibrary'] }, currentLibraryId() { - return this.currentLibrary ? this.currentLibrary.id : null + return this.currentLibrary?.id || null }, libraries() { return this.$store.getters['libraries/getSortedLibraries']() - }, - libraryScans() { - return this.$store.state.scanners.libraryScans } }, methods: { diff --git a/client/layouts/default.vue b/client/layouts/default.vue index 1f2acbd3..4f5e0fea 100644 --- a/client/layouts/default.vue +++ b/client/layouts/default.vue @@ -212,54 +212,6 @@ export default { this.libraryItemAdded(ab) }) }, - scanComplete(data) { - console.log('Scan complete received', data) - - let message = `${data.type === 'match' ? 'Match' : 'Scan'} "${data.name}" complete!` - let toastType = 'success' - if (data.error) { - message = `${data.type === 'match' ? 'Match' : 'Scan'} "${data.name}" finished with error:\n${data.error}` - toastType = 'error' - } else if (data.results) { - var scanResultMsgs = [] - var results = data.results - if (results.added) scanResultMsgs.push(`${results.added} added`) - if (results.updated) scanResultMsgs.push(`${results.updated} updated`) - if (results.removed) scanResultMsgs.push(`${results.removed} removed`) - if (results.missing) scanResultMsgs.push(`${results.missing} missing`) - if (!scanResultMsgs.length) message += '\nEverything was up to date' - else message += '\n' + scanResultMsgs.join('\n') - } else { - message = `${data.type === 'match' ? 'Match' : 'Scan'} "${data.name}" was canceled` - } - - var existingScan = this.$store.getters['scanners/getLibraryScan'](data.id) - if (existingScan && !isNaN(existingScan.toastId)) { - this.$toast.update(existingScan.toastId, { content: message, options: { timeout: 5000, type: toastType, closeButton: false, onClose: () => null } }, true) - } else { - this.$toast[toastType](message, { timeout: 5000 }) - } - - this.$store.commit('scanners/remove', data) - }, - onScanToastCancel(id) { - this.$root.socket.emit('cancel_scan', id) - }, - scanStart(data) { - data.toastId = this.$toast(`${data.type === 'match' ? 'Matching' : 'Scanning'} "${data.name}"...`, { timeout: false, type: 'info', draggable: false, closeOnClick: false, closeButton: CloseButton, closeButtonClassName: 'cancel-scan-btn', showCloseButtonOnHover: false, onClose: () => this.onScanToastCancel(data.id) }) - this.$store.commit('scanners/addUpdate', data) - }, - scanProgress(data) { - var existingScan = this.$store.getters['scanners/getLibraryScan'](data.id) - if (existingScan && !isNaN(existingScan.toastId)) { - data.toastId = existingScan.toastId - this.$toast.update(existingScan.toastId, { content: `Scanning "${existingScan.name}"... ${data.progress.progress || 0}%`, options: { timeout: false } }, true) - } else { - data.toastId = this.$toast(`Scanning "${data.name}"...`, { timeout: false, type: 'info', draggable: false, closeOnClick: false, closeButton: CloseButton, closeButtonClassName: 'cancel-scan-btn', showCloseButtonOnHover: false, onClose: () => this.onScanToastCancel(data.id) }) - } - - this.$store.commit('scanners/addUpdate', data) - }, taskStarted(task) { console.log('Task started', task) this.$store.commit('tasks/addUpdateTask', task) @@ -442,11 +394,6 @@ export default { this.socket.on('playlist_updated', this.playlistUpdated) this.socket.on('playlist_removed', this.playlistRemoved) - // Scan Listeners - this.socket.on('scan_start', this.scanStart) - this.socket.on('scan_complete', this.scanComplete) - this.socket.on('scan_progress', this.scanProgress) - // Task Listeners this.socket.on('task_started', this.taskStarted) this.socket.on('task_finished', this.taskFinished) diff --git a/client/store/scanners.js b/client/store/scanners.js index de154c3d..ccdc1791 100644 --- a/client/store/scanners.js +++ b/client/store/scanners.js @@ -1,5 +1,4 @@ export const state = () => ({ - libraryScans: [], providers: [ { text: 'Google Books', @@ -72,26 +71,8 @@ export const state = () => ({ ] }) -export const getters = { - getLibraryScan: state => id => { - return state.libraryScans.find(ls => ls.id === id) - } -} +export const getters = {} -export const actions = { +export const actions = {} -} - -export const mutations = { - addUpdate(state, data) { - const index = state.libraryScans.findIndex(lib => lib.id === data.id) - if (index >= 0) { - state.libraryScans.splice(index, 1, data) - } else { - state.libraryScans.push(data) - } - }, - remove(state, data) { - state.libraryScans = state.libraryScans.filter(scan => scan.id !== data.id) - } -} \ No newline at end of file +export const mutations = {} \ No newline at end of file diff --git a/client/store/tasks.js b/client/store/tasks.js index 9277d412..96e7e5b8 100644 --- a/client/store/tasks.js +++ b/client/store/tasks.js @@ -9,7 +9,8 @@ export const getters = { return state.tasks.filter(t => t.data?.libraryItemId === libraryItemId) }, getRunningLibraryScanTask: (state) => (libraryId) => { - return state.tasks.find(t => t.data?.libraryId === libraryId && !t.isFinished) + const libraryScanActions = ['library-scan', 'library-match-all'] + return state.tasks.find(t => libraryScanActions.includes(t.action) && t.data?.libraryId === libraryId && !t.isFinished) } } diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index 9c593ff2..10a77b2a 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -775,6 +775,13 @@ class LibraryController { }) } + /** + * GET: /api/libraries/:id/matchall + * Quick match all library items. Book libraries only. + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ async matchAll(req, res) { if (!req.user.isAdminOrUp) { Logger.error(`[LibraryController] Non-root user attempted to match library items`, req.user) diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index d17dbd15..b6f2f285 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -69,7 +69,7 @@ class LibraryItem extends Model { * * @param {number} offset * @param {number} limit - * @returns {Promise[]>} LibraryItem + * @returns {Promise} LibraryItem */ static getLibraryItemsIncrement(offset, limit, where = null) { return this.findAll({ diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js index ebce3607..616baf29 100644 --- a/server/scanner/Scanner.js +++ b/server/scanner/Scanner.js @@ -12,6 +12,7 @@ const Author = require('../objects/entities/Author') const Series = require('../objects/entities/Series') const LibraryScanner = require('./LibraryScanner') const CoverManager = require('../managers/CoverManager') +const TaskManager = require('../managers/TaskManager') class Scanner { constructor() { } @@ -280,6 +281,14 @@ class Scanner { return false } + /** + * Quick match library items + * + * @param {import('../objects/Library')} library + * @param {import('../objects/LibraryItem')[]} libraryItems + * @param {LibraryScan} libraryScan + * @returns {Promise} false if scan canceled + */ async matchLibraryItemsChunk(library, libraryItems, libraryScan) { for (let i = 0; i < libraryItems.length; i++) { const libraryItem = libraryItems[i] @@ -313,6 +322,11 @@ class Scanner { return true } + /** + * Quick match all library items for library + * + * @param {import('../objects/Library')} library + */ async matchLibraryItems(library) { if (library.mediaType === 'podcast') { Logger.error(`[Scanner] matchLibraryItems: Match all not supported for podcasts yet`) @@ -330,11 +344,14 @@ class Scanner { const libraryScan = new LibraryScan() libraryScan.setData(library, 'match') LibraryScanner.librariesScanning.push(libraryScan.getScanEmitData) - SocketAuthority.emitter('scan_start', libraryScan.getScanEmitData) - + const taskData = { + libraryId: library.id + } + const task = TaskManager.createAndAddTask('library-match-all', `Matching books in "${library.name}"`, null, true, taskData) Logger.info(`[Scanner] matchLibraryItems: Starting library match scan ${libraryScan.id} for ${libraryScan.libraryName}`) let hasMoreChunks = true + let isCanceled = false while (hasMoreChunks) { const libraryItems = await Database.libraryItemModel.getLibraryItemsIncrement(offset, limit, { libraryId: library.id }) if (!libraryItems.length) { @@ -347,6 +364,7 @@ class Scanner { const shouldContinue = await this.matchLibraryItemsChunk(library, oldLibraryItems, libraryScan) if (!shouldContinue) { + isCanceled = true break } } @@ -354,13 +372,15 @@ class Scanner { if (offset === 0) { Logger.error(`[Scanner] matchLibraryItems: Library has no items ${library.id}`) libraryScan.setComplete('Library has no items') + task.setFailed(libraryScan.error) } else { libraryScan.setComplete() + task.setFinished(isCanceled ? 'Canceled' : libraryScan.scanResultsString) } delete LibraryScanner.cancelLibraryScan[libraryScan.libraryId] LibraryScanner.librariesScanning = LibraryScanner.librariesScanning.filter(ls => ls.id !== library.id) - SocketAuthority.emitter('scan_complete', libraryScan.getScanEmitData) + TaskManager.taskFinished(task) } } module.exports = new Scanner()