From f36645dfd995bd9ce3c50a60b724865a06fbab7a Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Wed, 16 Sep 2026 07:58:25 -0500 Subject: [PATCH] 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//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) Claude-Session: https://claude.ai/code/session_01JpiSWBmtQkR5bcgpHWY4ME --- .gitignore | 4 + tests/js/audiomotion-paths.test.js | 150 ++++++++++++++++++ web/lang/en_gb.php | 16 +- web/lang/ru_ru.php | 16 +- .../assets/audioMotion-analyzer/src/help.txt | 33 +++- web/skins/classic/assets/version | 7 + 6 files changed, 208 insertions(+), 18 deletions(-) create mode 100644 tests/js/audiomotion-paths.test.js diff --git a/.gitignore b/.gitignore index c2aea7406..fdb55f0da 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/tests/js/audiomotion-paths.test.js b/tests/js/audiomotion-paths.test.js new file mode 100644 index 000000000..9a43296d0 --- /dev/null +++ b/tests/js/audiomotion-paths.test.js @@ -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//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); diff --git a/web/lang/en_gb.php b/web/lang/en_gb.php index c3a64187f..4f40dad70 100644 --- a/web/lang/en_gb.php +++ b/web/lang/en_gb.php @@ -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.
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( diff --git a/web/lang/ru_ru.php b/web/lang/ru_ru.php index 4b7881546..496ec7caf 100644 --- a/web/lang/ru_ru.php +++ b/web/lang/ru_ru.php @@ -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( diff --git a/web/skins/classic/assets/audioMotion-analyzer/src/help.txt b/web/skins/classic/assets/audioMotion-analyzer/src/help.txt index b3a0eb5d7..85447bffe 100644 --- a/web/skins/classic/assets/audioMotion-analyzer/src/help.txt +++ b/web/skins/classic/assets/audioMotion-analyzer/src/help.txt @@ -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". diff --git a/web/skins/classic/assets/version b/web/skins/classic/assets/version index 1585d4ae2..8ef47f7be 100644 --- a/web/skins/classic/assets/version +++ b/web/skins/classic/assets/version @@ -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