1 |
|
2 | "use strict";
|
3 |
|
4 | const assert = require('assert');
|
5 | const Path = require('path');
|
6 | const Async = require('async');
|
7 | const request = require('request');
|
8 |
|
9 | const debug = require('debug')('upnpserver:contentHandlers:tmdbAPI');
|
10 | const logger = require('../logger');
|
11 | const NamedSemaphore = require('../util/namedSemaphore');
|
12 |
|
13 | const REQUEST_PER_SECOND = 3;
|
14 | const SIMULATED_REQUEST_COUNT = 4;
|
15 |
|
16 | const IMAGE_LANGUAGES = "fr,en,null";
|
17 |
|
18 | class TmdbAPI {
|
19 | constructor(apiKey, configuration) {
|
20 | this.configuration = configuration;
|
21 |
|
22 | try {
|
23 | var movieDB = require('moviedb');
|
24 |
|
25 | this._movieDB = movieDB(apiKey);
|
26 |
|
27 | this._imagesSemaphore = new NamedSemaphore("tmdbImages");
|
28 |
|
29 | this._initialize();
|
30 |
|
31 | } catch (x) {
|
32 | logger.info("Can not use moviedb, please install moviedb npm package");
|
33 | }
|
34 | }
|
35 |
|
36 | _initialize() {
|
37 |
|
38 | this._lastRun = Date.now();
|
39 | this._remaining = 30;
|
40 |
|
41 | var next = (task, callback) => {
|
42 |
|
43 |
|
44 | var now = Date.now();
|
45 |
|
46 | var r = this._remaining + Math.floor((now - this._lastRun) / (1000 / REQUEST_PER_SECOND));
|
47 | if (r <= SIMULATED_REQUEST_COUNT) {
|
48 |
|
49 |
|
50 | setTimeout(() => {
|
51 | next(task, callback);
|
52 | }, (1000 / REQUEST_PER_SECOND));
|
53 | return;
|
54 | }
|
55 |
|
56 |
|
57 |
|
58 | this._remaining--;
|
59 | this._lastRun = Date.now();
|
60 | task(callback);
|
61 | };
|
62 |
|
63 | this.callQueue = Async.queue(next, SIMULATED_REQUEST_COUNT);
|
64 | }
|
65 |
|
66 | _loadConfiguration(callback) {
|
67 | if (this._tmdbConfiguration) {
|
68 | return callback(null, this._tmdbConfiguration);
|
69 | }
|
70 |
|
71 | this._newRequest((callback) => {
|
72 | if (this._tmdbConfiguration) {
|
73 | return callback();
|
74 | }
|
75 |
|
76 | this._movieDB.configuration((error, configuration, req) => {
|
77 | if (error) {
|
78 | return callback(error);
|
79 | }
|
80 | this._processRequestResponse(req);
|
81 |
|
82 | debug("_loadConfiguration", "tmdb configuration loaded !", configuration);
|
83 |
|
84 | this._tmdbConfiguration = configuration;
|
85 | callback();
|
86 | });
|
87 |
|
88 | }, () => {
|
89 | callback(null, this._tmdbConfiguration);
|
90 | });
|
91 | }
|
92 |
|
93 | _processRequestResponse(response) {
|
94 | var remaining = response.headers['x-ratelimit-remaining'];
|
95 | if (remaining === undefined) {
|
96 | return;
|
97 | }
|
98 |
|
99 | this._remaining = parseInt(remaining, 10);
|
100 | this._lastRun = Date.now();
|
101 | }
|
102 |
|
103 | _newRequest(func, callback) {
|
104 | this.callQueue.push(func, callback);
|
105 | }
|
106 |
|
107 | searchTvShow(name, years, callback) {
|
108 | assert(typeof (callback) === "function", "Invalid callback parameter");
|
109 |
|
110 | debug("searchTvInfos", "name=", name, "years=", years);
|
111 |
|
112 | if (!this._movieDB) {
|
113 | return callback();
|
114 | }
|
115 |
|
116 | this._newRequest((callback) => {
|
117 |
|
118 | this._movieDB.searchTv({ query: name }, (error, res, req) => {
|
119 | debug("searchTvInfos", "name=", name, "response=", res);
|
120 | if (error) {
|
121 | return callback(error);
|
122 | }
|
123 |
|
124 | this._processRequestResponse(req);
|
125 |
|
126 | if (res.total_results === 1) {
|
127 | callback(null, res.results[0].id);
|
128 | return;
|
129 | }
|
130 |
|
131 | if (res.total_results > 1) {
|
132 | var r = res.results.find(
|
133 | (r) => (r.name.toLowerCase() === name.toLowerCase() || r.original_name.toLowerCase() === name.toLowerCase()));
|
134 | if (r) {
|
135 | callback(null, res.results[0].id);
|
136 | return;
|
137 | }
|
138 | }
|
139 |
|
140 | callback();
|
141 | });
|
142 | }, callback);
|
143 | }
|
144 |
|
145 | loadTvShow(key, previousInfos, callback) {
|
146 | assert(typeof (callback) === "function", "Invalid callback parameter");
|
147 |
|
148 | debug("loadTvShow", "key=", key);
|
149 |
|
150 | previousInfos = previousInfos || {};
|
151 |
|
152 | var lang = this.configuration.lang || 'fr';
|
153 |
|
154 | var ret = {};
|
155 |
|
156 | this._newRequest((callback) => {
|
157 | var p = { id: key, language: lang };
|
158 |
|
159 | if (previousInfos.$timestamp) {
|
160 | p.ifModifiedSince = new Date(previousInfos.$timestamp);
|
161 | }
|
162 | if (previousInfos.$etag && !this.configuration.ignoreETAG) {
|
163 | p.ifNoneMatch = previousInfos.$etag;
|
164 | }
|
165 |
|
166 | this._movieDB.tvInfo(p, (error, infos, req) => {
|
167 | if (error) {
|
168 | debug("loadTvShow", "error=", error);
|
169 | return callback(error);
|
170 | }
|
171 | this._processRequestResponse(req);
|
172 |
|
173 | if (req && req.header.etag) {
|
174 | if (previousInfos.$etag === req.header.etag && !this.configuration.ignoreETAG) {
|
175 | debug("loadTvShow", "TvInfos has same etag !");
|
176 |
|
177 | Object.assign(ret, previousInfos);
|
178 | ret.$timestamp = (new Date()).toUTCString();
|
179 |
|
180 | return callback();
|
181 | }
|
182 |
|
183 | ret.$etag = req.header.etag;
|
184 | }
|
185 |
|
186 | ret.$timestamp = (new Date()).toUTCString();
|
187 |
|
188 | if (infos.backdrop_path) {
|
189 | delete infos.backdrop_path;
|
190 |
|
191 | }
|
192 | |
193 |
|
194 |
|
195 |
|
196 | if (infos.poster_path) {
|
197 | delete infos.poster_path;
|
198 |
|
199 | }
|
200 |
|
201 | Object.assign(ret, infos);
|
202 |
|
203 | callback();
|
204 | });
|
205 |
|
206 | }, (error) => {
|
207 | var tasks = [];
|
208 |
|
209 | this._newRequest((callback) => {
|
210 | var p = {
|
211 | id: key,
|
212 | language: lang,
|
213 | include_image_language: IMAGE_LANGUAGES
|
214 | };
|
215 |
|
216 | if (previousInfos.$imagesTimestamp) {
|
217 | p.ifModifiedSince = new Date(previousInfos.$imagesTimestamp);
|
218 | }
|
219 | if (previousInfos.$imagesEtag && !this.configuration.ignoreETAG) {
|
220 | p.ifNoneMatch = previousInfos.$imagesEtag;
|
221 | }
|
222 |
|
223 | this._movieDB.tvImages(p, (error, infos, req) => {
|
224 | if (error) {
|
225 | console.error(error);
|
226 | return callback(error);
|
227 | }
|
228 | this._processRequestResponse(req);
|
229 |
|
230 | if (req && req.header.etag) {
|
231 | if (previousInfos.$imagesEtag === req.header.etag && !this.configuration.ignoreETAG) {
|
232 | debug("TvImages has same etag !");
|
233 |
|
234 | ret.$imagesTimestamp = (new Date()).toUTCString();
|
235 | ret.posters = previousInfos.posters;
|
236 | ret.backdrops = previousInfos.backdrops;
|
237 | return callback();
|
238 | }
|
239 |
|
240 | ret.$imagesEtag = req.header.etag;
|
241 | }
|
242 | ret.$imagesTimestamp = (new Date()).toUTCString();
|
243 |
|
244 |
|
245 |
|
246 | if (infos.posters && infos.posters.length) {
|
247 | ret.posters = infos.posters.map((poster) =>
|
248 | ({ path: poster.file_path, width: poster.width, height: poster.height })
|
249 | );
|
250 | }
|
251 |
|
252 | if (infos.backdrops && infos.backdrops.length) {
|
253 | ret.backdrops = infos.backdrops.map((poster) =>
|
254 | ({ path: poster.file_path, width: poster.width, height: poster.height })
|
255 | );
|
256 | }
|
257 |
|
258 | callback();
|
259 | });
|
260 | }, (error) => {
|
261 | if (error) {
|
262 | return callback(error);
|
263 | }
|
264 |
|
265 | debug("loadTvShow", "key=", key, "returns=", ret);
|
266 |
|
267 | var seasons = ret.seasons || [];
|
268 | if (!seasons.length) {
|
269 | callback(null, ret);
|
270 | return;
|
271 | }
|
272 |
|
273 | delete ret.seasons;
|
274 |
|
275 | var previousSeasons = previousInfos.seasons || {};
|
276 |
|
277 | var tasks = [];
|
278 | seasons.forEach((season, idx) => {
|
279 | tasks.push((callback) => {
|
280 | var i = idx;
|
281 |
|
282 | this._syncSeason(key, seasons[i], previousSeasons[i], callback);
|
283 | });
|
284 | });
|
285 |
|
286 | Async.parallel(tasks, (error) => {
|
287 | debug("loadTvShow", "Seasons synced ! error=", error);
|
288 |
|
289 | if (error) {
|
290 | return callback(error);
|
291 | }
|
292 |
|
293 | ret.seasons = seasons;
|
294 |
|
295 | callback(null, ret);
|
296 | });
|
297 | });
|
298 | });
|
299 | }
|
300 |
|
301 | _syncSeason(tvKey, season, previousSeason, callback) {
|
302 | debug("_syncSeason", "key=", tvKey, "season=", season.season_number);
|
303 |
|
304 | previousSeason = previousSeason || {};
|
305 | var lang = this.configuration.lang || 'fr';
|
306 |
|
307 | this._newRequest((callback) => {
|
308 | var p = { id: tvKey, season_number: season.season_number, language: lang };
|
309 |
|
310 | if (previousSeason.$timestamp) {
|
311 | p.ifModifiedSince = new Date(previousSeason.$timestamp);
|
312 | }
|
313 | if (previousSeason.$etag && !this.configuration.ignoreETAG) {
|
314 | p.ifNoneMatch = season.$etag;
|
315 | }
|
316 |
|
317 | this._movieDB.tvSeasonInfo(p, (error, infos, req) => {
|
318 |
|
319 | debug("_syncSeason", "tvSeasonInfo response key=", tvKey, "season=", season.season_number, "error=", error);
|
320 | if (error) {
|
321 | return callback(error);
|
322 | }
|
323 | this._processRequestResponse(req);
|
324 |
|
325 | if (req && req.header.etag) {
|
326 | if (previousSeason.$etag === req.header.etag && !this.configuration.ignoreETAG) {
|
327 | debug("_syncSeason", "SAME ETAG ! (SEASON)");
|
328 |
|
329 | Object.assign(season, previousSeason);
|
330 | season.$timestamp = (new Date()).toUTCString();
|
331 |
|
332 | return callback();
|
333 | }
|
334 |
|
335 | season.$etag = req.header.etag;
|
336 | }
|
337 |
|
338 | season.$timestamp = (new Date()).toUTCString();
|
339 |
|
340 |
|
341 |
|
342 | if (!infos.production_code) {
|
343 | delete infos.production_code;
|
344 | }
|
345 | if (!infos.overview) {
|
346 | delete infos.overview;
|
347 | }
|
348 |
|
349 | if (infos.poster_path) {
|
350 | delete infos.poster_path;
|
351 |
|
352 | }
|
353 |
|
354 | infos.episodes.forEach((episode) => {
|
355 |
|
356 |
|
357 | if (!episode.still_path) {
|
358 | delete episode.still_path;
|
359 | }
|
360 | if (!episode.production_code) {
|
361 | delete episode.production_code;
|
362 | }
|
363 | if (!episode.overview) {
|
364 | delete episode.overview;
|
365 | }
|
366 | if (episode.crew && !episode.crew.length) {
|
367 | delete episode.crew;
|
368 | }
|
369 | if (episode.guest_stars && !episode.guest_stars.length) {
|
370 | delete episode.guest_stars;
|
371 | }
|
372 |
|
373 | if (episode.still_path) {
|
374 |
|
375 |
|
376 | }
|
377 | if (episode.poster_path) {
|
378 |
|
379 |
|
380 | }
|
381 |
|
382 | (episode.crew || []).forEach((c) => {
|
383 | if (c.profile_path) {
|
384 |
|
385 | }
|
386 | if (!c.profile_path) {
|
387 | delete c.profile_path;
|
388 | }
|
389 | });
|
390 | (episode.guest_stars || []).forEach((c) => {
|
391 | if (c.profile_path) {
|
392 |
|
393 | }
|
394 | if (!c.profile_path) {
|
395 | delete c.profile_path;
|
396 | }
|
397 | });
|
398 | });
|
399 |
|
400 | Object.assign(season, infos);
|
401 |
|
402 | callback();
|
403 | });
|
404 | }, () => {
|
405 | debug("_syncSeason", "tvSeasonInfo images key=", tvKey, "season=", season.season_number);
|
406 |
|
407 | this._newRequest((callback) => {
|
408 | var p = {
|
409 | id: tvKey,
|
410 | season_number: season.season_number,
|
411 | language: lang,
|
412 | include_image_language: IMAGE_LANGUAGES
|
413 | };
|
414 |
|
415 | if (previousSeason.$imagesTimestamp) {
|
416 | p.ifModifiedSince = new Date(previousSeason.$imagesTimestamp);
|
417 | }
|
418 |
|
419 | if (previousSeason.$imagesEtag && !this.configuration.ignoreETAG) {
|
420 | p.ifNoneMatch = previousSeason.$imagesEtag;
|
421 | }
|
422 |
|
423 | this._movieDB.tvSeasonImages(p, (error, infos, req) => {
|
424 | debug("_syncSeason", "tvSeasonInfo IMAGES response key=", tvKey, "season=", season.season_number, "error=", error);
|
425 |
|
426 | if (error) {
|
427 | console.error(error);
|
428 | return callback(error);
|
429 | }
|
430 | this._processRequestResponse(req);
|
431 |
|
432 | if (req && req.header.etag) {
|
433 | if (previousSeason.$imagesEtag === req.header.etag && !this.configuration.ignoreETAG) {
|
434 | debug("_syncSeason", "SeasonImages has same etag !");
|
435 |
|
436 | season.posters = previousSeason.posters;
|
437 | season.backdrops = previousSeason.backdrops;
|
438 | season.$imagesTimestamp = (new Date()).toUTCString();
|
439 |
|
440 | return callback();
|
441 | }
|
442 |
|
443 | season.$imagesEtag = req.header.etag;
|
444 | }
|
445 | season.$imagesTimestamp = (new Date()).toUTCString();
|
446 |
|
447 |
|
448 |
|
449 | if (infos.posters && infos.posters.length) {
|
450 | season.posters = infos.posters.map((poster) => ({
|
451 | path: poster.file_path,
|
452 | width: poster.width,
|
453 | height: poster.height
|
454 | }));
|
455 | }
|
456 |
|
457 | if (infos.backdrops && infos.backdrops.length) {
|
458 | season.backdrops = infos.backdrops.map((poster) => ({
|
459 | path: poster.file_path,
|
460 | width: poster.width,
|
461 | height: poster.height
|
462 | }));
|
463 | }
|
464 | callback();
|
465 | });
|
466 |
|
467 | }, () => {
|
468 | var episodes = season.episodes || [];
|
469 | var previousEpisodes = previousSeason.episodes || {};
|
470 |
|
471 | var tasks = [];
|
472 | episodes.forEach((season, idx) => {
|
473 | tasks.push((callback) => {
|
474 | var i = idx;
|
475 |
|
476 | this._syncEpisode(tvKey, season.season_number, episodes[i], previousEpisodes[i], callback);
|
477 | });
|
478 | });
|
479 |
|
480 | Async.parallel(tasks, (error) => {
|
481 | debug("loadTvShow", "Episodes synced ! error=", error);
|
482 |
|
483 | if (error) {
|
484 | return callback(error);
|
485 | }
|
486 |
|
487 | callback();
|
488 | });
|
489 | });
|
490 | });
|
491 | }
|
492 |
|
493 | _syncEpisode(tvKey, seasonNumber, episode, previousEpisode, callback) {
|
494 | debug("_syncEpisode", "tvSeasonInfo images key=", tvKey, "season=", seasonNumber, "episode=", episode.episode_number);
|
495 |
|
496 | previousEpisode = previousEpisode || {};
|
497 | var lang = this.configuration.lang || 'fr';
|
498 |
|
499 | this._newRequest((callback) => {
|
500 | var p = {
|
501 | id: tvKey,
|
502 | season_number: seasonNumber,
|
503 | episode_number: episode.episode_number,
|
504 | language: lang,
|
505 | include_image_language: IMAGE_LANGUAGES
|
506 | };
|
507 |
|
508 | if (previousEpisode.$imagesTimestamp) {
|
509 | p.ifModifiedSince = new Date(previousEpisode.$imagesTimestamp);
|
510 | }
|
511 |
|
512 | if (previousEpisode.$imagesEtag && !this.configuration.ignoreETAG) {
|
513 | p.ifNoneMatch = previousEpisode.$imagesEtag;
|
514 | }
|
515 |
|
516 | this._movieDB.tvEpisodeImages(p, (error, infos, req) => {
|
517 | debug("_syncEpisode", "tvEpisodeImages IMAGES response key=", tvKey, "season=", seasonNumber, "episode=", episode.episode_number, "error=", error);
|
518 |
|
519 | if (error) {
|
520 | console.error(error);
|
521 | return callback(error);
|
522 | }
|
523 | this._processRequestResponse(req);
|
524 |
|
525 | if (req && req.header.etag) {
|
526 | if (previousEpisode.$imagesEtag === req.header.etag && !this.configuration.ignoreETAG) {
|
527 | debug("_syncEpisode", "EpisodesImages has same etag !");
|
528 |
|
529 | episode.posters = previousEpisode.posters;
|
530 | episode.stills = previousEpisode.stills;
|
531 | episode.$imagesTimestamp = (new Date()).toUTCString();
|
532 |
|
533 | return callback();
|
534 | }
|
535 |
|
536 | episode.$imagesEtag = req.header.etag;
|
537 | }
|
538 | episode.$imagesTimestamp = (new Date()).toUTCString();
|
539 |
|
540 |
|
541 |
|
542 | if (infos.posters && infos.posters.length) {
|
543 | episode.posters = infos.posters.map((poster) => ({
|
544 | path: poster.file_path,
|
545 | width: poster.width,
|
546 | height: poster.height
|
547 | }));
|
548 | }
|
549 |
|
550 | if (infos.stills && infos.stills.length) {
|
551 | episode.stills = infos.stills.map((poster) => ({
|
552 | path: poster.file_path,
|
553 | width: poster.width,
|
554 | height: poster.height
|
555 | }));
|
556 | }
|
557 | callback();
|
558 | });
|
559 | }, () => {
|
560 | callback();
|
561 | });
|
562 | }
|
563 |
|
564 | loadImage(dest, path, update, callback) {
|
565 | debug("loadImage", "Load image dest=", dest);
|
566 |
|
567 | if (!this._movieDB) {
|
568 | return callback();
|
569 | }
|
570 |
|
571 | this._loadConfiguration((error, configuration) => {
|
572 | if (error) {
|
573 | return callback(error);
|
574 | }
|
575 | if (!configuration) {
|
576 | return callback(new Error("Can not load configuration"));
|
577 | }
|
578 |
|
579 | this._imagesSemaphore.take(path, (semaphore) => {
|
580 | dest.stat((error, stats) => {
|
581 | if (!update && stats) {
|
582 | return callback(null, stats);
|
583 | }
|
584 |
|
585 | if (error && error.code !== 'ENOENT') {
|
586 | return callback(error);
|
587 | }
|
588 |
|
589 | var imageURL = configuration.images.secure_base_url + "original/" + path;
|
590 |
|
591 | var options = {
|
592 | uri: imageURL,
|
593 | headers: {}
|
594 | };
|
595 |
|
596 | if (stats) {
|
597 | options.headers['If-Modified-Since'] = stats.mtime.toUTCString();
|
598 | }
|
599 |
|
600 | debug("loadImage", "Request image dest=", dest, "url=", options);
|
601 |
|
602 | dest.createWriteStream({ autoClose: true }, (error, outputStream) => {
|
603 | if (error) {
|
604 | logger.error(error);
|
605 | return;
|
606 | }
|
607 |
|
608 | var stream = request.get(options);
|
609 |
|
610 | stream.on('response', (response) => {
|
611 |
|
612 | this._processRequestResponse(response);
|
613 |
|
614 | if (response.statusCode === 200) {
|
615 | stream.pipe(outputStream);
|
616 | return;
|
617 | }
|
618 |
|
619 | if (response.statusCode === 304) {
|
620 | debug("Image is not modified !");
|
621 | return;
|
622 | }
|
623 |
|
624 | logger.info("StatusCode=" + response.statusCode);
|
625 | logger.info("ContentType=" + response.headers['content-type']);
|
626 | });
|
627 |
|
628 | stream.on('end', () => {
|
629 |
|
630 |
|
631 | dest.stat(callback);
|
632 | });
|
633 | });
|
634 | });
|
635 | });
|
636 | });
|
637 | }
|
638 | }
|
639 |
|
640 |
|
641 | module.exports = TmdbAPI;
|