node-red-contrib-hik-media-buffer 1.2.3 → 1.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,79 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('hik-download', {
3
+ category: 'Hikvision',
4
+ color: '#00b4d8',
5
+ defaults: {
6
+ name: { value: "" },
7
+ host: { value: "" },
8
+ channels: { value: "1" },
9
+ protocol: { value: "http" },
10
+ port: { value: "80" },
11
+ user: { value: "admin" },
12
+ pass: { value: "" },
13
+ startTime: { value: "" },
14
+ endTime: { value: "" }
15
+ },
16
+ inputs: 1,
17
+ outputs: 1,
18
+ icon: "font-awesome/fa-video-camera",
19
+ label: function() { return this.name || "Hik Download Video"; }
20
+ });
21
+ </script>
22
+
23
+ <script type="text/html" data-template-name="hik-download">
24
+ <div class="form-row">
25
+ <label for="node-input-name"><i class="fa fa-tag"></i> Nome</label>
26
+ <input type="text" id="node-input-name">
27
+ </div>
28
+
29
+ <hr>
30
+ <h4>Configurazione NVR</h4>
31
+ <div class="form-row">
32
+ <label for="node-input-host"><i class="fa fa-globe"></i> IP NVR</label>
33
+ <input type="text" id="node-input-host" placeholder="Es: 192.168.1.100">
34
+ </div>
35
+
36
+ <div class="form-row">
37
+ <label for="node-input-channels"><i class="fa fa-list-ol"></i> Canali</label>
38
+ <input type="text" id="node-input-channels" placeholder="Es: 1 o 1,3,5 o 1-4">
39
+ </div>
40
+
41
+ <div class="form-row">
42
+ <label for="node-input-user"><i class="fa fa-user"></i> Utente</label>
43
+ <input type="text" id="node-input-user">
44
+ </div>
45
+
46
+ <div class="form-row">
47
+ <label for="node-input-pass"><i class="fa fa-lock"></i> Password NVR</label>
48
+ <input type="password" id="node-input-pass">
49
+ </div>
50
+
51
+ <div class="form-row">
52
+ <label for="node-input-startTime"><i class="fa fa-calendar"></i> Data Inizio</label>
53
+ <input type="text" id="node-input-startTime" placeholder="Formato: AAAA-MM-GG ORE:MIN:SEC (Es: 2026-07-14 10:00:00)">
54
+ </div>
55
+
56
+ <div class="form-row">
57
+ <label for="node-input-endTime"><i class="fa fa-calendar"></i> Data Fine</label>
58
+ <input type="text" id="node-input-endTime" placeholder="Formato: AAAA-MM-GG ORE:MIN:SEC (Es: 2026-07-14 10:05:00)">
59
+ </div>
60
+
61
+ <div class="form-row">
62
+ <label for="node-input-protocol"><i class="fa fa-shield"></i> Protocollo</label>
63
+ <select id="node-input-protocol" style="width: 70%">
64
+ <option value="http">HTTP</option>
65
+ <option value="https">HTTPS</option>
66
+ </select>
67
+ </div>
68
+
69
+ <div class="form-row">
70
+ <label for="node-input-port"><i class="fa fa-plug"></i> Porta</label>
71
+ <select id="node-input-port" style="width: 70%">
72
+ <option value="80">80</option>
73
+ <option value="443">443</option>
74
+ <option value="85">85</option>
75
+ <option value="8000">8000</option>
76
+ <option value="8800">8800</option>
77
+ </select>
78
+ </div>
79
+ </script>
@@ -0,0 +1,154 @@
1
+ const axios = require('axios');
2
+ const https = require('https');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const mhocDigest = require('@mhoc/axios-digest-auth');
6
+ const DigestAuthClass = mhocDigest.default;
7
+
8
+ module.exports = function(RED) {
9
+ function HikDownloadNode(config) {
10
+ RED.nodes.createNode(this, config);
11
+ const node = this;
12
+
13
+ node.protocol = config.protocol || "http";
14
+ node.host = config.host;
15
+ node.port = config.port || "80";
16
+ node.user = config.user;
17
+ node.pass = config.pass;
18
+ node.channels = config.channels || "1";
19
+ node.startTime = config.startTime || "";
20
+ node.endTime = config.endTime || "";
21
+
22
+ const httpsAgent = new https.Agent({ rejectUnauthorized: false });
23
+
24
+ // Helper per decifrare i canali (es: "1,3" o "1-3")
25
+ function parseChannels(channelsStr) {
26
+ let channels = [];
27
+ if (!channelsStr) return [1];
28
+ let parts = channelsStr.toString().split(',');
29
+ parts.forEach(part => {
30
+ part = part.trim();
31
+ if (part.includes('-')) {
32
+ let range = part.split('-');
33
+ let start = parseInt(range[0]);
34
+ let end = parseInt(range[1]);
35
+ if (!isNaN(start) && !isNaN(end)) {
36
+ for (let i = start; i <= end; i++) channels.push(i);
37
+ }
38
+ } else {
39
+ let ch = parseInt(part);
40
+ if (!isNaN(ch)) channels.push(ch);
41
+ }
42
+ });
43
+ return [...new Set(channels)].sort((a, b) => a - b);
44
+ }
45
+
46
+ node.on('input', async function(msg) {
47
+ if (msg.payload !== true) return;
48
+
49
+ node.status({ fill: "blue", shape: "dot", text: "Verifica parametri..." });
50
+
51
+ const host = msg.nvr_host || node.host;
52
+ const port = msg.nvr_port || node.port;
53
+ const user = msg.nvr_user || node.user;
54
+ const pass = msg.nvr_pass || node.pass;
55
+ const protocol = msg.nvr_protocol || node.protocol;
56
+ const startTimeRaw = msg.startTime || node.startTime;
57
+ const endTimeRaw = msg.endTime || node.endTime;
58
+ const targetChannels = parseChannels(msg.channels || node.channels);
59
+
60
+ if (!startTimeRaw || !endTimeRaw) {
61
+ node.status({ fill: "red", shape: "ring", text: "Date mancanti!" });
62
+ node.error("startTime o endTime non configurati.");
63
+ return;
64
+ }
65
+
66
+ // 🌟 1. LOGICA DI PULIZIA DELLE DATE PER HIKVISION (es. "2026-07-14 10:00:00" -> "20260714T100000")
67
+ const pulisciDataNVR = (dataStr) => {
68
+ return dataStr
69
+ .replace(/[-:\s]/g, '') // Rimuove trattini, due punti e spazi
70
+ .split('.')[0] // Rimuove eventuali millisecondi
71
+ .replace('Z', ''); // Rimuove la Z se presente
72
+ };
73
+
74
+ const startTimeNVR = pulisciDataNVR(startTimeRaw);
75
+ const endTimeNVR = pulisciDataNVR(endTimeRaw);
76
+
77
+ // 🌟 2. CREAZIONE CARTELLA DI DESTINAZIONE (C:\download\YYYY-MM-DD)
78
+ // Estraiamo la data del giorno dallo startTime (i primi 10 caratteri, es: "2026-07-14")
79
+ const dataGiorno = startTimeRaw.substring(0, 10).replace(/\//g, '-');
80
+ const baseDir = "C:\\download";
81
+ const targetDir = path.join(baseDir, dataGiorno);
82
+
83
+ try {
84
+ if (!fs.existsSync(targetDir)) {
85
+ fs.mkdirSync(targetDir, { recursive: true });
86
+ }
87
+ } catch (err) {
88
+ node.status({ fill: "red", shape: "dot", text: "Errore cartella" });
89
+ node.error(`Impossibile creare la cartella ${targetDir}: ${err.message}`);
90
+ return;
91
+ }
92
+
93
+ const digest = new DigestAuthClass({ username: user, password: pass });
94
+
95
+ // 🌟 3. CICLO DI DOWNLOAD E SALVATAGGIO SU DISCO
96
+ for (let index = 0; index < targetChannels.length; index++) {
97
+ const ch = targetChannels[index];
98
+ const trackId = ch + "01";
99
+
100
+ node.status({
101
+ fill: "blue",
102
+ shape: "dot",
103
+ text: `Download Ch ${ch} (${index + 1}/${targetChannels.length})...`
104
+ });
105
+
106
+ const playbackURI = `rtsp://${host}/Streaming/tracks/${trackId}/?starttime=${startTimeNVR}&endtime=${endTimeNVR}`;
107
+ const downloadUrl = `${protocol}://${host}:${port}/ISAPI/ContentMgmt/download?playbackURI=${encodeURIComponent(playbackURI)}`;
108
+
109
+ try {
110
+ const response = await digest.request({
111
+ method: 'GET',
112
+ url: downloadUrl,
113
+ responseType: 'arraybuffer',
114
+ httpsAgent: protocol === 'https' ? httpsAgent : undefined,
115
+ timeout: 120000 // 2 minuti di timeout per i video corposi
116
+ });
117
+
118
+ if (response.status === 200 || response.status === 206) {
119
+ const videoBuffer = Buffer.from(response.data);
120
+
121
+ // Prepariamo il nome del file ripulito (es: NVR_Cam1_100000.mp4)
122
+ const nomeCondominio = (msg.nvr_name || node.name || 'NVR').replace(/[^a-zA-Z0-9]/g, '_');
123
+ const oraInizio = startTimeNVR.split('T')[1] || startTimeNVR;
124
+ const finalFileName = `${nomeCondominio}_Cam${ch}_${oraInizio}.mp4`;
125
+ const finalFilePath = path.join(targetDir, finalFileName);
126
+
127
+ // Scrittura fisica del file su disco C:\download\data\file.mp4
128
+ fs.writeFileSync(finalFilePath, videoBuffer);
129
+ node.log(`File salvato con successo in: ${finalFilePath}`);
130
+
131
+ // Inviamo comunque il messaggio avanti nel flusso nel caso servisse ad altri nodi
132
+ let outMsg = RED.util.cloneMessage(msg);
133
+ outMsg.payload = finalFilePath; // Spedisce il percorso del file salvato come testo
134
+ outMsg.channel = ch;
135
+ outMsg.filename = finalFileName;
136
+ node.send(outMsg);
137
+ } else {
138
+ node.error(`Errore NVR Ch ${ch} Status: ${response.status}`);
139
+ }
140
+
141
+ } catch (err) {
142
+ node.error(`Errore download video Cam ${ch}: ${err.message}`);
143
+ }
144
+
145
+ if (targetChannels.length > 1 && index < targetChannels.length - 1) {
146
+ await new Promise(resolve => setTimeout(resolve, 1000));
147
+ }
148
+ }
149
+
150
+ node.status({ fill: "green", shape: "dot", text: `Salvati in C:\\download\\${dataGiorno}` });
151
+ });
152
+ }
153
+ RED.nodes.registerType("hik-download", HikDownloadNode);
154
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-hik-media-buffer",
3
- "version": "1.2.3",
3
+ "version": "1.2.5",
4
4
  "description": "Ottiene buffer video e immagine da camere Hikvision via ISAPI",
5
5
  "keywords": [
6
6
  "node-red",
@@ -16,7 +16,8 @@
16
16
  "node-red": {
17
17
  "nodes": {
18
18
  "hik-media-buffer": "hik-media-buffer.js",
19
- "hik-snapshot": "hik-snapshot.js"
19
+ "hik-snapshot": "hik-snapshot.js",
20
+ "hik-download": "hik-download.js"
20
21
  }
21
22
  },
22
23
  "dependencies": {