fix: point the audioMotion-analyzer install instructions at the ES module

The library is AGPL-3.0-or-later so it is not shipped, and the admin installs
it at skins/<skin>/assets/audioMotion-analyzer/src/audioMotion-analyzer.js.
The instructions for doing that had drifted from what the code loads:

- help.txt and the OPTIONS_WHATTODISPLAY help gave the bare
  https://cdn.jsdelivr.net/npm/audiomotion-analyzer@X.X.X URL, which resolves
  to the package's "main" entry, the minified UMD bundle dist/index.js. The
  install path is the package's src/ ES module, so the URL needs the explicit
  /src/audioMotion-analyzer.js suffix. Same for the download links in the
  AudioMotionVersionNotInstalled and AudioMotionVersionWrongVersion messages.
- The install path was written as /skins/MySkin/..., a placeholder that does
  not correspond to any skin.
- RequiresAudioMotionEnabled named only the file, not where it goes.
- assets/version documented every other asset in that directory but not this
  one, leaving no explanation for the otherwise empty directory.

help.txt no longer restates the required version, so 4.5.4 stays declared only
by SUPPORTED_AUDIO_MOTION_ANALYZER_VERSION as intended, and assets/version
points at that constant rather than duplicating it.

Add tests/js/audiomotion-paths.test.js to hold the PHP feature probe, the
dynamic import, help.txt and both lang catalogues to the same path, the same
download URL and the same version.

Also gitignore the installed library so a local install is not committed back
into a GPL-2.0 tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpiSWBmtQkR5bcgpHWY4ME
This commit is contained in:
Isaac ConnorandClaude Opus 5 committed 2026-09-20 18:17:43 -05:00
1 parent 8b14fba2db
commit f36645dfd9
6 files changed
+208 -18

No files matched your search

+4
View File
@@ -219,3 +219,7 @@ _codeql_detected_source_root
.grepai/
.codegraph/
*.bak
# audioMotion-analyzer is AGPL-3.0-or-later and is installed by the
# administrator rather than shipped; see its src/help.txt.
web/skins/*/assets/audioMotion-analyzer/src/audioMotion-analyzer.js
+150
View File
@@ -0,0 +1,150 @@
'use strict';
// The audioMotion-analyzer library is not shipped with ZoneMinder (it is
// AGPL-3.0-or-later), so the admin installs it by hand following instructions
// that live in four places: the skin's PHP feature probe, the dynamic import in
// the skin JS, the help.txt beside the install location, and the translated
// help/strings. They drifted apart once already, sending people to a URL that
// serves a different build of the library than the one the import expects.
// These tests pin them to each other.
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const root = path.join(__dirname, '../..');
const read = (p) => fs.readFileSync(path.join(root, p), 'utf8');
const INSTALL_PATH = 'assets/audioMotion-analyzer/src/audioMotion-analyzer.js';
const functionsPhp = read('web/skins/classic/includes/functions.php');
const analyzerJs = read('web/skins/classic/js/audioMotionAnalyzer.js');
const helpTxt = read('web/skins/classic/' + INSTALL_PATH.replace(/[^/]+$/, 'help.txt'));
const assetsVersion = read('web/skins/classic/assets/version');
const enGb = read('web/lang/en_gb.php');
const ruRu = read('web/lang/ru_ru.php');
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
console.log(' ok ' + name);
passed++;
} catch (e) {
console.error(' FAIL ' + name);
console.error(' ' + e.message);
failed++;
}
}
console.log('install path agreement');
test('AUDIO_MOTION_ENABLED probes the path the skin JS imports', () => {
const probe = functionsPhp.match(/define\("AUDIO_MOTION_ENABLED",\s*file_exists\("([^"]+)"\)\)/);
assert.ok(probe, 'AUDIO_MOTION_ENABLED define not found in functions.php');
// skins/$skin/assets/... -> assets/...
const probed = probe[1].replace(/^skins\/\$skin\//, '');
assert.strictEqual(probed, INSTALL_PATH);
const imported = analyzerJs.match(/import\('([^']+audioMotion-analyzer\.js)'\)/);
assert.ok(imported, 'dynamic import of the library not found in audioMotionAnalyzer.js');
// The import is relative to skins/<skin>/js/, so ../assets/... is assets/...
assert.strictEqual(imported[1].replace(/^\.\.\//, ''), INSTALL_PATH);
});
test('help.txt tells the admin the path the code actually looks at', () => {
assert.ok(
helpTxt.includes('skins/classic/' + INSTALL_PATH),
'help.txt does not name skins/classic/' + INSTALL_PATH);
});
test('the help text does not use a placeholder skin name', () => {
for (const [name, text] of [['help.txt', helpTxt], ['en_gb.php', enGb], ['ru_ru.php', ruRu]]) {
assert.ok(!/MySkin/.test(text), name + ' still refers to the "MySkin" placeholder');
}
});
console.log('download instructions');
// The package root serves "main", the minified UMD bundle in dist/. Only the
// explicit /src/ path serves the ES module that the dynamic import needs, so
// the bare URL may appear only on a line that warns against using it.
const LINK = /https:\/\/cdn\.jsdelivr\.net\/npm\/audiomotion-analyzer@[^\s'",)]*/g;
// The Russian phrase is written with \u escapes so this file stays ASCII
// and passes utils/check-homoglyphs.py.
const RU_DO_NOT_USE = '\u041d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435';
const isWarning = (line) => /\bbare\b|Do not use/i.test(line) || line.includes(RU_DO_NOT_USE);
test('every jsDelivr link offered for download points at src/audioMotion-analyzer.js', () => {
for (const [name, text] of [['help.txt', helpTxt], ['en_gb.php', enGb], ['ru_ru.php', ruRu], ['assets/version', assetsVersion]]) {
let offered = 0;
for (const line of text.split('\n')) {
for (const match of line.match(LINK) || []) {
if (isWarning(line)) continue;
// "~~" is the lang catalogues' line-break marker, not part of the URL.
const link = match.replace(/~~$/, '');
offered++;
assert.ok(
link.endsWith('/src/audioMotion-analyzer.js'),
name + ' offers ' + link + ', which serves the UMD bundle, not the ES module');
}
}
assert.ok(offered, name + ' offers no jsDelivr download link');
}
});
test('isWarning only exempts lines that tell you not to use the URL', () => {
assert.ok(isWarning('Do not use the bare https://cdn.jsdelivr.net/npm/audiomotion-analyzer@X.X.X URL'));
assert.ok(!isWarning('https://cdn.jsdelivr.net/npm/audiomotion-analyzer@X.X.X/src/audioMotion-analyzer.js'));
});
console.log('version agreement');
const supported = analyzerJs.match(/SUPPORTED_AUDIO_MOTION_ANALYZER_VERSION\s*=\s*'([^']+)'/);
test('audioMotionAnalyzer.js declares the supported version', () => {
assert.ok(supported, 'SUPPORTED_AUDIO_MOTION_ANALYZER_VERSION not found');
assert.match(supported[1], /^\d+\.\d+\.\d+$/);
});
test('assets/version documents the same version the code requires', () => {
const entry = assetsVersion.match(/audioMotion-analyzer - (\d+\.\d+\.\d+)/);
assert.ok(entry, 'assets/version has no audioMotion-analyzer entry');
assert.strictEqual(entry[1], supported[1]);
});
test('help.txt does not hard-code a second copy of the version', () => {
assert.ok(
!helpTxt.includes(supported[1]),
'help.txt repeats the version ' + supported[1] + '; it should point at ' +
'SUPPORTED_AUDIO_MOTION_ANALYZER_VERSION instead');
});
console.log('translation keys');
test('en_gb and ru_ru define the same AudioMotion keys', () => {
const keys = (text) => (text.match(/'(AudioMotion\w+|RequiresAudioMotionEnabled)'\s*=>/g) || [])
.map((m) => m.match(/'([^']+)'/)[1]).sort();
assert.deepStrictEqual(keys(enGb), keys(ruRu));
assert.ok(keys(enGb).length >= 4, 'expected the AudioMotion* strings to be present');
});
test('the version-check strings use the placeholders monitor.js substitutes', () => {
const monitorJs = read('web/skins/classic/views/js/monitor.js');
const substituted = (monitorJs.match(/replaceAll\('\{(\w+)\}'/g) || [])
.map((m) => m.match(/\{(\w+)\}/)[1]);
assert.ok(substituted.length, 'monitor.js substitutes no AudioMotion placeholders');
for (const [name, text] of [['en_gb.php', enGb], ['ru_ru.php', ruRu]]) {
const used = new Set((text.match(/\{(AudioMotionVersion\w+)\}/g) || []).map((m) => m.slice(1, -1)));
for (const ph of used) {
assert.ok(
substituted.includes(ph),
name + ' uses {' + ph + '}, which monitor.js never substitutes');
}
}
});
console.log('');
console.log(passed + ' passed, ' + failed + ' failed');
process.exit(failed ? 1 : 0);
+9 -7
View File
@@ -144,8 +144,8 @@ $SLANG = array(
'AttrStartWeekday' => 'Start Weekday',
'AttrEndWeekday' => 'End Weekday',
'AudioMotionVersionOK' => 'Correct version installed "{AudioMotionVersionInstalled}"',
'AudioMotionVersionNotInstalled' => 'Requires audio motion analyzer version "{AudioMotionVersionRequired}" to be installed~~Download link: https://cdn.jsdelivr.net/npm/audiomotion-analyzer@{AudioMotionVersionRequired} or https://github.com/hvianna/audioMotion-analyzer/releases/tag/{AudioMotionVersionRequired}',
'AudioMotionVersionWrongVersion' => 'The required analyzer version is "{AudioMotionVersionRequired}", but you have "{AudioMotionVersionInstalled}" installed~~Download link: https://cdn.jsdelivr.net/npm/audiomotion-analyzer@{AudioMotionVersionRequired} or https://github.com/hvianna/audioMotion-analyzer/releases/tag/{AudioMotionVersionRequired}',
'AudioMotionVersionNotInstalled' => 'Requires audio motion analyzer version "{AudioMotionVersionRequired}" to be installed~~Download link: https://cdn.jsdelivr.net/npm/audiomotion-analyzer@{AudioMotionVersionRequired}/src/audioMotion-analyzer.js or https://github.com/hvianna/audioMotion-analyzer/releases/tag/{AudioMotionVersionRequired}',
'AudioMotionVersionWrongVersion' => 'The required analyzer version is "{AudioMotionVersionRequired}", but you have "{AudioMotionVersionInstalled}" installed~~Download link: https://cdn.jsdelivr.net/npm/audiomotion-analyzer@{AudioMotionVersionRequired}/src/audioMotion-analyzer.js or https://github.com/hvianna/audioMotion-analyzer/releases/tag/{AudioMotionVersionRequired}',
'Auth' => 'Authentication',
'AutoStopTimeout' => 'Auto Stop Timeout',
'AvgBrScore' => 'Avg.<br/>Score',
@@ -702,7 +702,7 @@ $SLANG = array(
'ReplaySingle' => 'Single Event',
'ReportEventAudit' => 'Audit Events Report',
'RequestMissing' => 'The request is missing',
'RequiresAudioMotionEnabled' => 'Requires installation of the file "audioMotion-analyzer.js"',
'RequiresAudioMotionEnabled' => 'Requires installation of the file "skins/classic/assets/audioMotion-analyzer/src/audioMotion-analyzer.js"',
'ResetEventCounts' => 'Reset Event Counts',
'RestrictedCameraIds' => 'Restricted Camera Ids',
'RestrictedMonitors' => 'Restricted Monitors',
@@ -1167,10 +1167,12 @@ Always: A zmc process will run and immediately connect and stay connected.~~~~
'OPTIONS_WHATTODISPLAY' => array(
'Help' => '
On the Watch, Montage, Event page, you can display either a video stream, or an audio stream visualization, or both a video stream and an audio visualization.~~
To display the audio motion visualization, install the file "/skins/MySkin/assets/audioMotion-analyzer/src/audioMotion-analyzer.js".~~
This file can be downloaded from the following links:~~
https://cdn.jsdelivr.net/npm/audiomotion-analyzer@X.X.X where X.X.X is the version number~~
https://github.com/hvianna/audioMotion-analyzer/releases
The visualization is drawn by the audioMotion-analyzer library, which is licensed AGPL-3.0-or-later and so is not shipped with ZoneMinder. To display it, install the library as "skins/classic/assets/audioMotion-analyzer/src/audioMotion-analyzer.js" under your web directory, substituting your own skin name for "classic" if you use a different skin.~~
Download it from one of the following, where X.X.X is the required version:~~
https://cdn.jsdelivr.net/npm/audiomotion-analyzer@X.X.X/src/audioMotion-analyzer.js~~
https://github.com/hvianna/audioMotion-analyzer/releases/tag/X.X.X (the file is under src/ in the source tarball)~~
Do not use the bare https://cdn.jsdelivr.net/npm/audiomotion-analyzer@X.X.X URL: it serves the minified UMD bundle rather than the ES module this path expects.~~
The required version is reported below this setting, and is set by SUPPORTED_AUDIO_MOTION_ANALYZER_VERSION in skins/classic/js/audioMotionAnalyzer.js.
',
),
'FUNCTION_ANALYSIS_ENABLED' => array(
+9 -7
View File
@@ -139,8 +139,8 @@ $SLANG = array(
'AttrSystemLoad' => 'Нагрузка проц.',
'AttrTotalScore' => 'Сумм. оценка',
'AudioMotionVersionOK' => 'Установлена корректная версия "{AudioMotionVersionInstalled}"',
'AudioMotionVersionNotInstalled' => 'Требуется установка audio motion analyzer версии "{AudioMotionVersionRequired}"~~Ссылка для загрузки: https://cdn.jsdelivr.net/npm/audiomotion-analyzer@{AudioMotionVersionRequired} или https://github.com/hvianna/audioMotion-analyzer/releases/tag/{AudioMotionVersionRequired}',
'AudioMotionVersionWrongVersion' => 'Требуется версия анализатора "{AudioMotionVersionRequired}", но у Вас установлена "{AudioMotionVersionInstalled}"~~Ссылка для загрузки: https://cdn.jsdelivr.net/npm/audiomotion-analyzer@{AudioMotionVersionRequired} или https://github.com/hvianna/audioMotion-analyzer/releases/tag/{AudioMotionVersionRequired}',
'AudioMotionVersionNotInstalled' => 'Требуется установка audio motion analyzer версии "{AudioMotionVersionRequired}"~~Ссылка для загрузки: https://cdn.jsdelivr.net/npm/audiomotion-analyzer@{AudioMotionVersionRequired}/src/audioMotion-analyzer.js или https://github.com/hvianna/audioMotion-analyzer/releases/tag/{AudioMotionVersionRequired}',
'AudioMotionVersionWrongVersion' => 'Требуется версия анализатора "{AudioMotionVersionRequired}", но у Вас установлена "{AudioMotionVersionInstalled}"~~Ссылка для загрузки: https://cdn.jsdelivr.net/npm/audiomotion-analyzer@{AudioMotionVersionRequired}/src/audioMotion-analyzer.js или https://github.com/hvianna/audioMotion-analyzer/releases/tag/{AudioMotionVersionRequired}',
'Auth' => 'Авторизация',
'Auto' => 'Авто',
'AutoStopTimeout' => 'Тайм-аут автоостановки',
@@ -685,7 +685,7 @@ $SLANG = array(
'ReportEventAudit' => 'Отчёт о событиях аудита', // Edited - 2019-03-24
'Reports' => 'Отчеты',
'RequestMissing' => 'В запросе отсутствует',
'RequiresAudioMotionEnabled' => 'Требуется установка файла "audioMotion-analyzer.js"',
'RequiresAudioMotionEnabled' => 'Требуется установка файла "skins/classic/assets/audioMotion-analyzer/src/audioMotion-analyzer.js"',
'Reset' => 'Сбросить',
'ResetEventCounts' => 'Обнулить счетчик событий',
'Restart' => 'Перезапустить',
@@ -1232,10 +1232,12 @@ $OLANG = array(
'OPTIONS_WHATTODISPLAY' => array(
'Help' => '
На страницах живого просмотра, монтажа и просмотра события возможно отображение движения аудио.~~
Для этого необходимо установить файл "/skins/MySkin/assets/audioMotion-analyzer/src/audioMotion-analyzer.js".~~
Указанный файл можно скачать по ссылкам:~~
https://cdn.jsdelivr.net/npm/audiomotion-analyzer@X.X.X где X.X.X это номер версии~~
https://github.com/hvianna/audioMotion-analyzer/releases
Библиотека audioMotion-analyzer распространяется под лицензией AGPL-3.0-or-later и не входит в состав ZoneMinder. Для отображения визуализации необходимо установить файл "skins/classic/assets/audioMotion-analyzer/src/audioMotion-analyzer.js" в веб-каталоге, заменив "classic" на имя используемого скина.~~
Указанный файл можно скачать по ссылкам, где X.X.X это номер требуемой версии:~~
https://cdn.jsdelivr.net/npm/audiomotion-analyzer@X.X.X/src/audioMotion-analyzer.js~~
https://github.com/hvianna/audioMotion-analyzer/releases/tag/X.X.X (файл находится в каталоге src/ архива с исходным кодом)~~
Не используйте ссылку https://cdn.jsdelivr.net/npm/audiomotion-analyzer@X.X.X без пути: по ней отдаётся минифицированная UMD-сборка, а не ES-модуль, который ожидается по этому пути.~~
Требуемая версия указана ниже, под этой настройкой, и задаётся константой SUPPORTED_AUDIO_MOTION_ANALYZER_VERSION в файле skins/classic/js/audioMotionAnalyzer.js.
'
),
'FUNCTION_ANALYSIS_ENABLED' => array(
@@ -1,5 +1,30 @@
Audio motion visualization can be displayed on the Montage, Watch, and Event pages.
To do this, install the file "/skins/MySkin/assets/audioMotion-analyzer/src/audioMotion-analyzer.js".
This file can be downloaded from the following links:
https://cdn.jsdelivr.net/npm/audiomotion-analyzer@X.X.X where X.X.X is the version number
https://github.com/hvianna/audioMotion-analyzer/releases
It is drawn by the audioMotion-analyzer library, which ZoneMinder does not ship:
it is licensed AGPL-3.0-or-later, so it has to be installed by hand. Drop the
library next to this file, keeping the file name exactly as it is upstream:
skins/classic/assets/audioMotion-analyzer/src/audioMotion-analyzer.js
(substitute your own skin name for "classic" if you are not using that skin.)
Download it with, where X.X.X is the required version:
curl -L -o audioMotion-analyzer.js \
https://cdn.jsdelivr.net/npm/audiomotion-analyzer@X.X.X/src/audioMotion-analyzer.js
Mind the "/src/audioMotion-analyzer.js" on the end of that URL. Do not use the
bare https://cdn.jsdelivr.net/npm/audiomotion-analyzer@X.X.X URL: it serves the
package's "main" entry point, which is the minified UMD bundle (dist/index.js),
not the ES module this path expects.
The same file is in the upstream source tarball under src/:
https://github.com/hvianna/audioMotion-analyzer/releases/tag/X.X.X
The required version is not repeated here, to keep it in one place. It is
SUPPORTED_AUDIO_MOTION_ANALYZER_VERSION in skins/classic/js/audioMotionAnalyzer.js,
and the monitor edit page reports, under "Show", which version is required and
whether the installed one matches.
Note that the npm package is named "audiomotion-analyzer" (all lower case) while
the project, this directory and the file itself are "audioMotion-analyzer".
+7
View File
@@ -14,3 +14,10 @@ https://github.com/john-doherty/swiped-events
***********************************
mb.extruder Oct 24, 2018 with magor changes IgorA100
https://github.com/pupunzi/jquery.mb.extruder
***********************************
audioMotion-analyzer - 4.5.4 (required version, see
SUPPORTED_AUDIO_MOTION_ANALYZER_VERSION in ../js/audioMotionAnalyzer.js)
NOT BUNDLED - AGPL-3.0-or-later, installed by the administrator.
See audioMotion-analyzer/src/help.txt
https://github.com/hvianna/audioMotion-analyzer
https://cdn.jsdelivr.net/npm/audiomotion-analyzer@4.5.4/src/audioMotion-analyzer.js