node-red-contrib-hik-media-buffer 1.2.4 → 1.2.6

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.
package/hik-download.html CHANGED
@@ -1,11 +1,11 @@
1
1
  <script type="text/javascript">
2
2
  RED.nodes.registerType('hik-download', {
3
3
  category: 'Hikvision',
4
- color: '#f77f00',
4
+ color: '#00b4d8',
5
5
  defaults: {
6
6
  name: { value: "" },
7
7
  host: { value: "" },
8
- channels: { value: "1" }, // Rinominato in 'channels'
8
+ channels: { value: "" },
9
9
  protocol: { value: "http" },
10
10
  port: { value: "80" },
11
11
  user: { value: "admin" },
@@ -50,12 +50,12 @@
50
50
 
51
51
  <div class="form-row">
52
52
  <label for="node-input-startTime"><i class="fa fa-calendar"></i> Data Inizio</label>
53
- <input type="text" id="node-input-startTime" placeholder="Es: 20260703T134125">
53
+ <input type="datetime-local" id="node-input-startTime" step="1" style="width: 70%">
54
54
  </div>
55
55
 
56
56
  <div class="form-row">
57
57
  <label for="node-input-endTime"><i class="fa fa-calendar"></i> Data Fine</label>
58
- <input type="text" id="node-input-endTime" placeholder="Es: 20260703T134135">
58
+ <input type="datetime-local" id="node-input-endTime" step="1" style="width: 70%">
59
59
  </div>
60
60
 
61
61
  <div class="form-row">
package/hik-download.js CHANGED
@@ -1,5 +1,7 @@
1
1
  const axios = require('axios');
2
2
  const https = require('https');
3
+ const fs = require('fs');
4
+ const path = require('path');
3
5
  const mhocDigest = require('@mhoc/axios-digest-auth');
4
6
  const DigestAuthClass = mhocDigest.default;
5
7
 
@@ -13,17 +15,15 @@ module.exports = function(RED) {
13
15
  node.port = config.port || "80";
14
16
  node.user = config.user;
15
17
  node.pass = config.pass;
16
- node.channels = config.channels || "1"; // Ora accetta stringhe (es: "1,2,3" o "1-4")
18
+ node.channels = config.channels || "1";
17
19
  node.startTime = config.startTime || "";
18
20
  node.endTime = config.endTime || "";
19
21
 
20
22
  const httpsAgent = new https.Agent({ rejectUnauthorized: false });
21
23
 
22
- // 🌟 FUNZIONE DI SUPPORTO: Decifra la stringa dei canali (es: "1,3,5" o "1-3" -> [1, 2, 3, 5])
23
24
  function parseChannels(channelsStr) {
24
25
  let channels = [];
25
26
  if (!channelsStr) return [1];
26
-
27
27
  let parts = channelsStr.toString().split(',');
28
28
  parts.forEach(part => {
29
29
  part = part.trim();
@@ -32,18 +32,13 @@ module.exports = function(RED) {
32
32
  let start = parseInt(range[0]);
33
33
  let end = parseInt(range[1]);
34
34
  if (!isNaN(start) && !isNaN(end)) {
35
- for (let i = start; i <= end; i++) {
36
- channels.push(i);
37
- }
35
+ for (let i = start; i <= end; i++) channels.push(i);
38
36
  }
39
37
  } else {
40
38
  let ch = parseInt(part);
41
- if (!isNaN(ch)) {
42
- channels.push(ch);
43
- }
39
+ if (!isNaN(ch)) channels.push(ch);
44
40
  }
45
41
  });
46
- // Rimuove i duplicati e ordina i canali
47
42
  return [...new Set(channels)].sort((a, b) => a - b);
48
43
  }
49
44
 
@@ -59,10 +54,7 @@ module.exports = function(RED) {
59
54
  const protocol = msg.nvr_protocol || node.protocol;
60
55
  const startTimeRaw = msg.startTime || node.startTime;
61
56
  const endTimeRaw = msg.endTime || node.endTime;
62
-
63
- // Decodifichiamo i canali (usa quelli del msg se presenti, altrimenti quelli del nodo)
64
- const targetChannelsStr = msg.channels || node.channels;
65
- const targetChannels = parseChannels(targetChannelsStr);
57
+ const targetChannels = parseChannels(msg.channels || node.channels);
66
58
 
67
59
  if (!startTimeRaw || !endTimeRaw) {
68
60
  node.status({ fill: "red", shape: "ring", text: "Date mancanti!" });
@@ -70,18 +62,39 @@ module.exports = function(RED) {
70
62
  return;
71
63
  }
72
64
 
73
- const pulisciData = (dataStr) => {
74
- return dataStr
75
- .replace(/[-:]/g, '')
76
- .split('.')[0]
77
- .replace('Z', '');
65
+ // Pulizia date per l'NVR
66
+ const pulisciDataNVR = (dataStr) => {
67
+ let d = dataStr.replace(' ', 'T');
68
+ d = d.replace(/[-:]/g, '')
69
+ .split('.')[0]
70
+ .replace('Z', '');
71
+
72
+ if (d.includes('T') && d.split('T')[1].length === 4) {
73
+ d += '00';
74
+ }
75
+ return d;
78
76
  };
79
77
 
80
- const startTime = pulisciData(startTimeRaw);
81
- const endTime = pulisciData(endTimeRaw);
78
+ const startTimeNVR = pulisciDataNVR(startTimeRaw);
79
+ const endTimeNVR = pulisciDataNVR(endTimeRaw);
80
+
81
+ // Creazione cartella C:\download\YYYY-MM-DD
82
+ const dataGiorno = startTimeRaw.substring(0, 10).replace(/\//g, '-');
83
+ const baseDir = "C:\\download";
84
+ const targetDir = path.join(baseDir, dataGiorno);
85
+
86
+ try {
87
+ if (!fs.existsSync(targetDir)) {
88
+ fs.mkdirSync(targetDir, { recursive: true });
89
+ }
90
+ } catch (err) {
91
+ node.status({ fill: "red", shape: "dot", text: "Errore cartella" });
92
+ node.error(`Impossibile creare la cartella ${targetDir}: ${err.message}`);
93
+ return;
94
+ }
95
+
82
96
  const digest = new DigestAuthClass({ username: user, password: pass });
83
97
 
84
- // 🌟 AVVIAMO IL CICLO DI DOWNLOAD PER OGNI CANALE SELEZIONATO
85
98
  for (let index = 0; index < targetChannels.length; index++) {
86
99
  const ch = targetChannels[index];
87
100
  const trackId = ch + "01";
@@ -92,25 +105,52 @@ module.exports = function(RED) {
92
105
  text: `Download Ch ${ch} (${index + 1}/${targetChannels.length})...`
93
106
  });
94
107
 
95
- const playbackURI = `rtsp://${host}/Streaming/tracks/${trackId}/?starttime=${startTime}&endtime=${endTime}`;
96
- const downloadUrl = `${protocol}://${host}:${port}/ISAPI/ContentMgmt/download?playbackURI=${encodeURIComponent(playbackURI)}`;
108
+ // 🌟 1. GENERIAMO L'URI DI PLAYBACK ESATTAMENTE COME RICHIESTO
109
+ const playbackURIGrezzo = `rtsp://${host}:${port}/Streaming/tracks/${trackId}/?starttime=${startTimeNVR}&endtime=${endTimeNVR}`;
110
+ const playbackURIXml = playbackURIGrezzo.replace(/&/g, '&amp;');
111
+
112
+ const payloadDownload = `<?xml version="1.0" encoding="UTF-8"?>
113
+ <downloadRequest>
114
+ <playbackURI>${playbackURIXml}</playbackURI>
115
+ </downloadRequest>`;
97
116
 
98
117
  try {
118
+ // 🌟 2. GET DI DOWNLOAD CON PAYLOAD XML E RESPONSE 'STREAM'
99
119
  const response = await digest.request({
100
120
  method: 'GET',
101
- url: downloadUrl,
102
- responseType: 'arraybuffer',
103
- httpsAgent: protocol === 'https' ? httpsAgent : undefined,
104
- timeout: 90000
121
+ url: `${protocol}://${host}:${port}/ISAPI/ContentMgmt/download`,
122
+ data: payloadDownload,
123
+ headers: { "Content-Type": "application/xml" },
124
+ responseType: 'stream',
125
+ insecureHTTPParser: true,
126
+ httpsAgent: protocol === "https" ? httpsAgent : undefined,
127
+ timeout: 120000
105
128
  });
106
129
 
107
130
  if (response.status === 200 || response.status === 206) {
108
- // Creiamo un nuovo msg per non sovrapporre i flussi se sono multipli
131
+ const nomeCondominio = (msg.nvr_name || node.name || 'NVR').replace(/[^a-zA-Z0-9]/g, '_');
132
+ const oraInizio = startTimeNVR.split('T')[1] || startTimeNVR;
133
+ const finalFileName = `${nomeCondominio}_Cam${ch}_${oraInizio}.mp4`;
134
+ const finalFilePath = path.join(targetDir, finalFileName);
135
+
136
+ // 🌟 3. SALVATAGGIO TRAMITE STREAM (Ottimizzato per file grandi)
137
+ const writer = fs.createWriteStream(finalFilePath);
138
+ response.data.pipe(writer);
139
+
140
+ await new Promise((resolve, reject) => {
141
+ writer.on('finish', resolve);
142
+ writer.on('error', (err) => {
143
+ writer.close();
144
+ reject(err);
145
+ });
146
+ });
147
+
148
+ node.log(`File salvato in: ${finalFilePath}`);
149
+
109
150
  let outMsg = RED.util.cloneMessage(msg);
110
- outMsg.payload = Buffer.from(response.data);
151
+ outMsg.payload = finalFilePath;
111
152
  outMsg.channel = ch;
112
- outMsg.filename = `${msg.nvr_name || 'NVR'}_Cam${ch}_${startTime}.mp4`;
113
-
153
+ outMsg.filename = finalFileName;
114
154
  node.send(outMsg);
115
155
  } else {
116
156
  node.error(`Errore NVR Ch ${ch} Status: ${response.status}`);
@@ -120,13 +160,12 @@ module.exports = function(RED) {
120
160
  node.error(`Errore download video Cam ${ch}: ${err.message}`);
121
161
  }
122
162
 
123
- // Piccolo delay di sicurezza tra un download e l'altro per non stressare la CPU dell'NVR
124
163
  if (targetChannels.length > 1 && index < targetChannels.length - 1) {
125
164
  await new Promise(resolve => setTimeout(resolve, 1000));
126
165
  }
127
166
  }
128
167
 
129
- node.status({ fill: "green", shape: "dot", text: "Tutti i download completati!" });
168
+ node.status({ fill: "green", shape: "dot", text: `Salvati in C:\\download\\${dataGiorno}` });
130
169
  });
131
170
  }
132
171
  RED.nodes.registerType("hik-download", HikDownloadNode);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-hik-media-buffer",
3
- "version": "1.2.4",
3
+ "version": "1.2.6",
4
4
  "description": "Ottiene buffer video e immagine da camere Hikvision via ISAPI",
5
5
  "keywords": [
6
6
  "node-red",