1 |
|
2 | "use strict";
|
3 |
|
4 | const Path = require('path');
|
5 | const EventEmitter = require('events');
|
6 |
|
7 | const assert = require('assert');
|
8 | const debug = require('debug')('upnpserver:contentHandlers:MetasJson');
|
9 | const logger = require('../logger');
|
10 |
|
11 | const Abstract_Metas = require('./abstract_metas');
|
12 | const NodeWeakHashmap = require('../util/nodeWeakHashmap');
|
13 | const NamedSemaphore = require('../util/namedSemaphore');
|
14 |
|
15 | const METADATAS_DIRECTORY = ".upnpserver";
|
16 | const DIRECTORY_JSON = "";
|
17 |
|
18 | const JSON_NOT_FOUND_OR_INVALID = "***INVALID***";
|
19 |
|
20 | class MetasJson extends Abstract_Metas {
|
21 | constructor(configuration) {
|
22 | super(configuration);
|
23 |
|
24 | this._basePath = this._configuration.basePath;
|
25 |
|
26 | this._jsonSemaphores = new NamedSemaphore("json:" + this.name);
|
27 |
|
28 | if (!MetasJson._jsonCache) {
|
29 | MetasJson._jsonCache = new NodeWeakHashmap("json", 1000 * 60, false);
|
30 | }
|
31 | }
|
32 |
|
33 |
|
34 | initialize(contentDirectoryService, callback) {
|
35 |
|
36 | if (this._basePath) {
|
37 | this._baseURL = contentDirectoryService.newURL(this._basePath);
|
38 | }
|
39 |
|
40 | super.initialize(contentDirectoryService, callback);
|
41 | }
|
42 |
|
43 | static get METADATAS_DIRECTORY() {
|
44 | return METADATAS_DIRECTORY;
|
45 | }
|
46 |
|
47 | _searchDirectoryJSONFile(contentURL, metasContext, callback) {
|
48 | assert(typeof (callback) === "function", "Invalid callback parameter");
|
49 |
|
50 | this._jsonSemaphores.take(contentURL.path, (semaphore) => {
|
51 | this._searchDirectoryJSONFile0(contentURL, metasContext, (error, jsonContext, directoryInfos) => {
|
52 | semaphore.leave();
|
53 |
|
54 | debug("_searchDirectoryJSONFile", "return error=", error);
|
55 |
|
56 | if (error) {
|
57 | callback();
|
58 | return;
|
59 | }
|
60 |
|
61 | callback(null, jsonContext, directoryInfos);
|
62 | });
|
63 | });
|
64 | }
|
65 |
|
66 | _searchForKey(name, type, callback) {
|
67 | assert(typeof (callback) === "function", "Invalid callback parameter");
|
68 | callback(null);
|
69 | }
|
70 |
|
71 | _searchDirectoryJSONFile0(contentURL, metasContext, callback) {
|
72 | assert(typeof (callback) === "function", "Invalid callback parameter");
|
73 |
|
74 | var jsonURL = contentURL.join(METADATAS_DIRECTORY, DIRECTORY_JSON + "." + this.name + ".json");
|
75 |
|
76 | debug("_searchDirectoryJSONFile", "Directory: test", contentURL, "jsonURL=", jsonURL);
|
77 |
|
78 | var directoryInfos = {
|
79 | type: 'directory',
|
80 | base: false
|
81 | };
|
82 |
|
83 |
|
84 |
|
85 | this._loadJSON(jsonURL, false, (error, jsonContext) => {
|
86 | var content;
|
87 |
|
88 | debug("_searchDirectoryJSONFile", "loadJSON url=", jsonURL, "returns", jsonContext);
|
89 |
|
90 | if (jsonContext) {
|
91 | content = jsonContext.content;
|
92 |
|
93 | if (content.key && content.type === 'tvShow' && content.tvShowInfo) {
|
94 | return callback(null, jsonContext, directoryInfos);
|
95 | }
|
96 | }
|
97 |
|
98 | if (!this._baseURL) {
|
99 |
|
100 |
|
101 | debug("_searchDirectoryJSONFile", "no tmdb baseURL, exit !");
|
102 | return callback(error);
|
103 | }
|
104 |
|
105 |
|
106 | var loadBaseJSON = (k, jsonContext) => {
|
107 |
|
108 | debug("_searchDirectoryJSONFile", "loadBaseJSON=", k);
|
109 |
|
110 | this._computeJSONPathInBase(k, directoryInfos, (error, jsonURL, directoryInfos) => {
|
111 |
|
112 | debug("_searchDirectoryJSONFile", "computeJSONPath error=", error, "url=", jsonURL, "directoryInfos=", directoryInfos);
|
113 | if (error) {
|
114 | return callback(error);
|
115 | }
|
116 |
|
117 | if (!jsonURL) {
|
118 | return callback(null);
|
119 | }
|
120 |
|
121 | directoryInfos.base = true;
|
122 |
|
123 |
|
124 |
|
125 | this._loadJSON(jsonURL, true, (error, jsonContext) => {
|
126 | if (error) {
|
127 | return callback(error);
|
128 | }
|
129 |
|
130 | if (!jsonContext) {
|
131 | debug("_searchDirectoryJSONFile", "No json", jsonURL);
|
132 |
|
133 | this._updateInfos(k.key, content.type, {}, (error2, content) => {
|
134 | debug("_searchDirectoryJSONFile", "UpdateInfos key=", k.key, "type=", content.type,
|
135 | "returns error=", error2, "content=", content);
|
136 |
|
137 | if (error2) {
|
138 | return callback(error2);
|
139 | }
|
140 |
|
141 | if (!content) {
|
142 | return callback(error);
|
143 | }
|
144 |
|
145 | var ret = {
|
146 | key: k.key,
|
147 | type: k.type
|
148 | };
|
149 | ret[k.type] = content;
|
150 |
|
151 | this.saveJSON(jsonURL, ret, true, (error, jsonContext) => {
|
152 | if (error) {
|
153 | logger.error(error);
|
154 | }
|
155 |
|
156 | callback(null, jsonContext, directoryInfos);
|
157 | });
|
158 | });
|
159 | return;
|
160 | }
|
161 |
|
162 | callback(null, jsonContext, directoryInfos);
|
163 | });
|
164 | });
|
165 | };
|
166 |
|
167 |
|
168 |
|
169 |
|
170 | var reg;
|
171 | if (!reg && content && content.key && content.type) {
|
172 | reg = {
|
173 | key: content.key,
|
174 | type: content.type
|
175 | };
|
176 | }
|
177 |
|
178 | if (!reg) {
|
179 | reg = this._getKeyFromDirectoryName(contentURL.basename);
|
180 | }
|
181 |
|
182 | if (!reg && content && content.type) {
|
183 | debug("_searchDirectoryJSONFile", "Search for key=", contentURL.basename);
|
184 |
|
185 | this._searchForKey(contentURL.basename, content.type, (error, key) => {
|
186 | if (error) {
|
187 | return callback(error);
|
188 | }
|
189 |
|
190 | if (!key) {
|
191 | return callback();
|
192 | }
|
193 |
|
194 | content.key = key;
|
195 | reg = {
|
196 | key: key,
|
197 | type: content.type
|
198 | };
|
199 |
|
200 | this.saveJSON(jsonContext.url, jsonContext.content, jsonContext.isBaseURL, (error, jsonContext) => {
|
201 | if (error) {
|
202 | logger.error(error);
|
203 | }
|
204 | loadBaseJSON(reg, jsonContext);
|
205 | });
|
206 | });
|
207 | return;
|
208 | }
|
209 |
|
210 | if (!reg) {
|
211 |
|
212 | return callback(error);
|
213 | }
|
214 |
|
215 | loadBaseJSON(reg, jsonContext);
|
216 | });
|
217 | }
|
218 |
|
219 | _searchJSONFile(contentURL, metasContext, callback) {
|
220 | assert(typeof (callback) === "function", "Invalid callback parameter");
|
221 | this._jsonSemaphores.take(contentURL.path, (semaphore) => {
|
222 | this._searchJSONFile0(contentURL, metasContext, (error, jsonContext, directoryInfos) => {
|
223 | semaphore.leave();
|
224 |
|
225 | if (error) {
|
226 | callback();
|
227 | return;
|
228 | }
|
229 | callback(null, jsonContext, directoryInfos);
|
230 | });
|
231 | });
|
232 | }
|
233 |
|
234 | _searchJSONFile0(contentURL, metasContext, callback) {
|
235 | assert(typeof (callback) === "function", "Invalid callback parameter");
|
236 |
|
237 | var basename = contentURL.basename;
|
238 |
|
239 | var jsonURL = contentURL.join('..', METADATAS_DIRECTORY, basename + "." + this.name + ".json");
|
240 |
|
241 | debug("_searchJSONFile", "File: jsonURL=", jsonURL, "contentURL=", contentURL, "basename=", basename);
|
242 |
|
243 | var fileInfos = { type: 'file', base: false };
|
244 |
|
245 | var regBasename = basename.replace(/_/g, ' ');
|
246 | var season;
|
247 | var episode;
|
248 | var reg = /\bS(\d{1,})[-\s]*E(\d{1,})\b/i.exec(regBasename);
|
249 | if (reg) {
|
250 | fileInfos.season = parseInt(reg[1], 10);
|
251 | fileInfos.episode = parseInt(reg[2], 10);
|
252 | fileInfos.type = "tvShow";
|
253 |
|
254 | } else {
|
255 | reg = /\bE(\d{1,})\b/i.exec(regBasename);
|
256 | if (reg) {
|
257 | fileInfos.season = 0;
|
258 | fileInfos.episode = parseInt(reg[1], 10);
|
259 | fileInfos.type = "tvShow";
|
260 | }
|
261 | }
|
262 |
|
263 | debug("_searchJSONFile", "File no JSON season=", season, "episode=", episode);
|
264 |
|
265 | var searchInDirectory = () => {
|
266 |
|
267 | if (fileInfos.season === undefined || fileInfos.episode === undefined) {
|
268 |
|
269 | return callback();
|
270 | }
|
271 |
|
272 | var directoryURL = contentURL.join('..');
|
273 |
|
274 | debug("_searchJSONFile", "tvShow: directoryURL=", directoryURL, "contentURL=", contentURL);
|
275 |
|
276 | this._searchDirectoryJSONFile(directoryURL, metasContext, (error, jsonContext, directoryInfos) => {
|
277 | if (error) {
|
278 | return callback(error);
|
279 | }
|
280 | if (!jsonContext) {
|
281 | debug("_searchJSONFile", "tvShow => no JSON for url=", directoryURL);
|
282 | return callback();
|
283 | }
|
284 |
|
285 | directoryInfos.season = fileInfos.season;
|
286 | directoryInfos.episode = fileInfos.episode;
|
287 | directoryInfos.type = fileInfos.type;
|
288 |
|
289 | callback(null, jsonContext, directoryInfos);
|
290 | });
|
291 | };
|
292 |
|
293 | this._loadJSON(jsonURL, false, (error, jsonContext) => {
|
294 | if (error) {
|
295 | logger.error(error);
|
296 | }
|
297 |
|
298 | if (jsonContext) {
|
299 | return callback(error, jsonContext, fileInfos);
|
300 | }
|
301 |
|
302 |
|
303 |
|
304 | var resourceKey;
|
305 | if (metasContext.keys) {
|
306 | resourceKey = metasContext.keys[this.domainKey];
|
307 | }
|
308 |
|
309 | if (!resourceKey) {
|
310 |
|
311 | resourceKey = this._getKeyFromFileName(basename);
|
312 | }
|
313 |
|
314 | if (!resourceKey) {
|
315 |
|
316 | searchInDirectory();
|
317 | return;
|
318 | }
|
319 |
|
320 | this._computeJSONPathInBase(resourceKey, fileInfos, (error, jsonURL, fileInfos) => {
|
321 | if (error || !jsonURL) {
|
322 | searchInDirectory();
|
323 | return callback(error);
|
324 | }
|
325 |
|
326 | this._loadJSON(jsonURL, true, (error, jsonContext) => {
|
327 | if (error) {
|
328 | return callback(error);
|
329 | }
|
330 |
|
331 | if (!jsonContext) {
|
332 |
|
333 | searchInDirectory();
|
334 | return;
|
335 | }
|
336 |
|
337 | callback(null, jsonContext, fileInfos);
|
338 | });
|
339 | });
|
340 | });
|
341 | }
|
342 |
|
343 | |
344 |
|
345 |
|
346 | prepareMetas(contentInfos, metasContext, callback) {
|
347 | var contentURL = contentInfos.contentURL;
|
348 | assert(contentURL, "Invalid contentURL");
|
349 |
|
350 | if (this.enabled === false) {
|
351 | return callback();
|
352 | }
|
353 |
|
354 | if (contentInfos.stats.isDirectory()) {
|
355 | this._searchDirectoryJSONFile(contentURL, metasContext, (error, jsonContext, directoryInfos) => {
|
356 |
|
357 | if (error || !jsonContext) {
|
358 | return callback(error);
|
359 | }
|
360 |
|
361 | this._processFolder(contentInfos, metasContext, jsonContext, directoryInfos, (error, metas) => {
|
362 | if (error) {
|
363 | return callback(error);
|
364 | }
|
365 |
|
366 | metas = metas || {};
|
367 | var mt = metas[this.name];
|
368 | if (!mt) {
|
369 | mt = {};
|
370 | metas[this.name] = mt;
|
371 | }
|
372 |
|
373 | metas.resourceType = directoryInfos.type;
|
374 | if (directoryInfos.key) {
|
375 | mt.key = directoryInfos.key;
|
376 | }
|
377 | if (directoryInfos.base) {
|
378 | mt.base = directoryInfos.base;
|
379 | }
|
380 |
|
381 | callback(null, metas);
|
382 | });
|
383 | });
|
384 | return;
|
385 | }
|
386 |
|
387 | this._searchJSONFile(contentURL, metasContext, (error, jsonContext, fileInfos) => {
|
388 |
|
389 | if (error || !jsonContext) {
|
390 | return callback(error);
|
391 | }
|
392 |
|
393 | this._processFile(contentInfos, metasContext, jsonContext, fileInfos, (error, metas) => {
|
394 | if (error) {
|
395 | return callback(error);
|
396 | }
|
397 |
|
398 | metas = metas || {};
|
399 | var mt = metas[this.name];
|
400 | if (!mt) {
|
401 | mt = {};
|
402 | metas[this.name] = mt;
|
403 | }
|
404 |
|
405 | metas.resourceType = fileInfos.type;
|
406 | if (fileInfos.key) {
|
407 | mt.key = fileInfos.key;
|
408 | }
|
409 | if (fileInfos.base) {
|
410 | mt.base = fileInfos.base;
|
411 | }
|
412 |
|
413 | callback(null, metas);
|
414 | });
|
415 | });
|
416 | }
|
417 |
|
418 | _getKeyFromFileName(contentURL) {
|
419 | return null;
|
420 | }
|
421 |
|
422 | _getKeyFromDirectoryName(contentURL) {
|
423 | return null;
|
424 | }
|
425 |
|
426 | _processFile(contentInfos, metasContext, jsonContext, fileInfos, callback) {
|
427 | callback();
|
428 | }
|
429 |
|
430 | _processFolder(contentInfos, metasContext, jsonContext, directoryInfos, callback) {
|
431 | callback();
|
432 | }
|
433 |
|
434 | _getBaseDirectoryFromNode(node, callback) {
|
435 | var attributes = node.attributes;
|
436 | var contentURL = node.contentURL;
|
437 | var resourceType = attributes.resourceType;
|
438 |
|
439 | if (resourceType === "directory") {
|
440 | return contentURL.join(METADATAS_DIRECTORY, this.name);
|
441 | }
|
442 |
|
443 | return contentURL.join('..', METADATAS_DIRECTORY, this.name);
|
444 | }
|
445 |
|
446 | saveJSON(jsonURL, content, isBaseURL, callback) {
|
447 | assert(typeof (callback) === "function", "Invalid callback parameter");
|
448 |
|
449 | debug("saveJSON", "try to save jsonURL=", jsonURL, "content=", content);
|
450 |
|
451 |
|
452 | var newJsonContext = {
|
453 | url: jsonURL,
|
454 | content: content,
|
455 | isBaseURL: isBaseURL
|
456 | };
|
457 |
|
458 | MetasJson._jsonCache.put({ id: jsonURL.path }, newJsonContext);
|
459 |
|
460 | var buffer = JSON.stringify(content, null, 2);
|
461 |
|
462 | jsonURL.writeContent(buffer, "utf8", (error) => {
|
463 | if (error) {
|
464 | return callback(error);
|
465 | }
|
466 |
|
467 | callback(null, newJsonContext);
|
468 | });
|
469 | }
|
470 |
|
471 | _loadJSON(jsonURL, isBaseURL, callback) {
|
472 | assert(typeof (callback) === "function", "Invalid callback parameter");
|
473 | assert(typeof (isBaseURL) === "boolean", "Invalid isBaseURL parameter");
|
474 |
|
475 | debug("_loadJSON", "try to load jsonURL=", jsonURL, "isBaseURL=", isBaseURL);
|
476 |
|
477 | var jsonContext = MetasJson._jsonCache.get(jsonURL.path);
|
478 | if (jsonContext) {
|
479 | if (jsonContext instanceof EventEmitter) {
|
480 | var eventEmitter = jsonContext;
|
481 | debug("_loadJSON", "Wait for async result path=", jsonURL);
|
482 |
|
483 | eventEmitter.on('result', (jsonContext) => callback(null, jsonContext));
|
484 | eventEmitter.on('problem', (error) => callback( ));
|
485 | return;
|
486 | }
|
487 |
|
488 | if (jsonContext === JSON_NOT_FOUND_OR_INVALID) {
|
489 | debug("_loadJSON", "Return NOT FOUND");
|
490 | return callback(null, null);
|
491 | }
|
492 |
|
493 | debug("_loadJSON", "Return json from url=", jsonURL, "typeof json=", typeof (jsonContext));
|
494 | return callback(null, jsonContext);
|
495 | }
|
496 |
|
497 | var ev = new EventEmitter();
|
498 | MetasJson._jsonCache.put({ id: jsonURL.path }, ev);
|
499 |
|
500 | jsonURL.stat((error, stats) => {
|
501 | if (error) {
|
502 | debug("_loadJSON", "Stat returns error", error);
|
503 |
|
504 | var ex = new Error("Can not stat '" + jsonURL.path + "'");
|
505 | ex.reason = error;
|
506 | ex.url = jsonURL;
|
507 |
|
508 | MetasJson._jsonCache.put({ id: jsonURL.path }, JSON_NOT_FOUND_OR_INVALID);
|
509 |
|
510 | ev.emit('problem', ex);
|
511 |
|
512 |
|
513 | return callback();
|
514 | }
|
515 |
|
516 | jsonURL.readContent("utf8", (error, content) => {
|
517 |
|
518 |
|
519 | if (error) {
|
520 | debug("_loadJSON", "ReadContent returns error", error);
|
521 |
|
522 | MetasJson._jsonCache.put({ id: jsonURL.path }, JSON_NOT_FOUND_OR_INVALID);
|
523 |
|
524 | ev.emit('problem', error);
|
525 | logger.error(error);
|
526 |
|
527 | return callback();
|
528 | }
|
529 |
|
530 | var json;
|
531 | try {
|
532 | json = JSON.parse(content);
|
533 |
|
534 | } catch (x) {
|
535 | debug("_loadJSON", "JSON parsing throws exception", x);
|
536 |
|
537 | var ex = new Error("Can not parse JSON ");
|
538 | ex.reason = x;
|
539 | logger.error(ex);
|
540 |
|
541 | MetasJson._jsonCache.put({ id: jsonURL.path }, JSON_NOT_FOUND_OR_INVALID);
|
542 |
|
543 | ev.emit('problem', ex);
|
544 | logger.error(ex);
|
545 | return callback();
|
546 | }
|
547 |
|
548 | var jsonContext = {
|
549 | content: json,
|
550 | url: jsonURL,
|
551 | isBaseURL: isBaseURL
|
552 | };
|
553 |
|
554 | debug("_loadJSON", "jsonContext=", jsonContext);
|
555 |
|
556 | MetasJson._jsonCache.put({ id: jsonURL.path }, jsonContext);
|
557 |
|
558 | ev.emit('result', jsonContext);
|
559 |
|
560 | callback(null, jsonContext);
|
561 | });
|
562 | });
|
563 | }
|
564 | }
|
565 |
|
566 | module.exports = MetasJson;
|