diff --git a/scanner/scanner.go b/scanner/scanner.go index 93be6852..64cc2b1c 100644 --- a/scanner/scanner.go +++ b/scanner/scanner.go @@ -110,8 +110,22 @@ func (s *scanner) rescan(mediaFolder string, fullRescan bool) error { log.Debug("Scanning folder (full scan)", "folder", mediaFolder) } + progress := s.startProgressTracker(mediaFolder) + defer close(progress) + + err := folderScanner.Scan(log.NewContext(context.TODO()), lastModifiedSince, progress) + if err != nil { + log.Error("Error importing MediaFolder", "folder", mediaFolder, err) + } + + s.updateLastModifiedSince(mediaFolder, start) + return err +} + +func (s *scanner) startProgressTracker(mediaFolder string) chan uint32 { progress := make(chan uint32, 100) go func() { + s.broker.SendMessage(&events.ScanStatus{Scanning: true, Count: 0}) defer func() { s.broker.SendMessage(&events.ScanStatus{Scanning: false, Count: int64(s.status[mediaFolder].count)}) }() @@ -127,15 +141,7 @@ func (s *scanner) rescan(mediaFolder string, fullRescan bool) error { s.broker.SendMessage(&events.ScanStatus{Scanning: true, Count: int64(total)}) } }() - - err := folderScanner.Scan(log.NewContext(context.TODO()), lastModifiedSince, progress) - close(progress) - if err != nil { - log.Error("Error importing MediaFolder", "folder", mediaFolder, err) - } - - s.updateLastModifiedSince(mediaFolder, start) - return err + return progress } func (s *scanner) RescanAll(fullRescan bool) error { diff --git a/ui/src/actions/serverEvents.js b/ui/src/actions/serverEvents.js index 07a1bf86..aad78d25 100644 --- a/ui/src/actions/serverEvents.js +++ b/ui/src/actions/serverEvents.js @@ -1,12 +1,18 @@ -export const EVENT_SCAN_STATUS = 'ACTIVITY_SCAN_STATUS_UPD' +export const EVENT_SCAN_STATUS = 'EVENT_SCAN_STATUS' const actionsMap = { scanStatus: EVENT_SCAN_STATUS } export const processEvent = (data) => { let type = actionsMap[data.name] - if (!type) type = 'EVENT_UNKNOWN' + if (!type) type = data.name return { type, data: data.data, } } + +export const scanStatusUpdate = (data) => + processEvent({ + name: EVENT_SCAN_STATUS, + data: data, + }) diff --git a/ui/src/common/index.js b/ui/src/common/index.js index 18c8c9b8..856f5d64 100644 --- a/ui/src/common/index.js +++ b/ui/src/common/index.js @@ -21,4 +21,5 @@ export * from './StarButton' export * from './Title' export * from './SongBulkActions' export * from './useAlbumsPerPage' +export * from './useInterval' export * from './Writable' diff --git a/ui/src/common/useInterval.js b/ui/src/common/useInterval.js new file mode 100644 index 00000000..4ad0caa2 --- /dev/null +++ b/ui/src/common/useInterval.js @@ -0,0 +1,23 @@ +// From https://overreacted.io/making-setinterval-declarative-with-react-hooks/ + +import { useEffect, useRef } from 'react' + +export const useInterval = (callback, delay) => { + const savedCallback = useRef() + + // Remember the latest callback. + useEffect(() => { + savedCallback.current = callback + }, [callback]) + + // Set up the interval. + useEffect(() => { + function tick() { + savedCallback.current() + } + if (delay !== null) { + let id = setInterval(tick, delay) + return () => clearInterval(id) + } + }, [delay]) +} diff --git a/ui/src/layout/ActivityMenu.js b/ui/src/layout/ActivityMenu.js index 70c6c68a..52ab1308 100644 --- a/ui/src/layout/ActivityMenu.js +++ b/ui/src/layout/ActivityMenu.js @@ -1,16 +1,18 @@ -import React, { useState } from 'react' -import { useSelector } from 'react-redux' +import React, { useState, useEffect } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { fetchUtils } from 'react-admin' import { Menu, + MenuItem, Badge, CircularProgress, IconButton, makeStyles, Tooltip, - MenuItem, } from '@material-ui/core' import { FiActivity } from 'react-icons/fi' import subsonic from '../subsonic' +import { scanStatusUpdate } from '../actions' const useStyles = makeStyles((theme) => ({ wrapper: { @@ -18,8 +20,8 @@ const useStyles = makeStyles((theme) => ({ }, progress: { position: 'absolute', - top: -1, - left: 0, + top: 10, + left: 10, zIndex: 1, }, button: { @@ -30,31 +32,43 @@ const useStyles = makeStyles((theme) => ({ const ActivityMenu = () => { const classes = useStyles() const [anchorEl, setAnchorEl] = useState(null) - const scanStatus = useSelector((state) => state.activity.scanStatus) - const open = Boolean(anchorEl) + const scanStatus = useSelector((state) => state.activity.scanStatus) + const dispatch = useDispatch() - const handleMenu = (event) => setAnchorEl(event.currentTarget) - const handleClose = () => setAnchorEl(null) - const startScan = () => fetch(subsonic.url('startScan', null)) + const handleMenuOpen = (event) => setAnchorEl(event.currentTarget) + const handleCloseClose = () => setAnchorEl(null) + const triggerScan = () => fetch(subsonic.url('startScan')) + + // Get updated status on component mount + useEffect(() => { + fetchUtils + .fetchJson(subsonic.url('getScanStatus')) + .then((resp) => resp.json['subsonic-response']) + .then((data) => { + if (data.status === 'ok') { + dispatch(scanStatusUpdate(data.scanStatus)) + } + }) + }, [dispatch]) return (
- + {scanStatus.scanning && ( - + )} { horizontal: 'right', }} open={open} - onClose={handleClose} + onClose={handleCloseClose} > {`Scanned: ${scanStatus.count}`}