UNPKG

19.3 kBJavaScriptView Raw
1/*jslint node: true, esversion: 6, maxlen: 180 */
2"use strict";
3
4const assert = require('assert');
5const Path = require('path');
6const Async = require('async');
7const request = require('request');
8
9const debug = require('debug')('upnpserver:contentHandlers:tmdbAPI');
10const logger = require('../logger');
11const NamedSemaphore = require('../util/namedSemaphore');
12
13const REQUEST_PER_SECOND = 3;
14const SIMULATED_REQUEST_COUNT = 4;
15
16const IMAGE_LANGUAGES = "fr,en,null";
17
18class 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 // debug("Process new task");
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 // debug("Wait",400,"ms for next request remaining=",r);
49
50 setTimeout(() => {
51 next(task, callback);
52 }, (1000 / REQUEST_PER_SECOND));
53 return;
54 }
55
56 // console.log("r=",r);
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 // console.log(res);
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 // tasks.push((callback) => this.copyImage(tvInfo, "backdrop_path", tvPath, callback));
191 }
192 /*
193 * if (infos.created_by) { infos.created_by.forEach((creator) => { if (creator.profile_path) { tasks.push((callback) =>
194 * this.copyImage(creator.profile_path, tvPath, callback)); } }); }
195 */
196 if (infos.poster_path) {
197 delete infos.poster_path;
198 // tasks.push((callback) => this.copyImage(tvInfo, "poster_path", tvPath, callback));
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 // console.log("TvImages=",json.key, infos);
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 // debug("Season Infos=",util.inspect(infos, {depth: null}));
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 //tasks.push((callback) => this.copyImage(season.poster_path, tvPath, callback));
352 }
353
354 infos.episodes.forEach((episode) => {
355 // console.log("E=",episode);
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 //delete episode.still_path;
375 //tasks.push((callback) => this.copyImage(episode, "still_path", tvPath, callback));
376 }
377 if (episode.poster_path) {
378 //delete episode.poster_path;
379 //tasks.push((callback) => this.copyImage(episode, "poster_path", tvPath, callback));
380 }
381
382 (episode.crew || []).forEach((c) => {
383 if (c.profile_path) {
384 //tasks.push((callback) => this.copyImage(c.profile_path, tvPath, callback));
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 //tasks.push((callback) => this.copyImage(c.profile_path, tvPath, callback));
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 //console.log("SeasonImages=",json.key,season.season_number,infos);
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 //console.log("SeasonImages=",json.key,season.season_number,infos);
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; // +"?api_key="+this.movieDB.api_key;
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 //console.log("ImageResponse=",response.headers);
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); // 200
625 logger.info("ContentType=" + response.headers['content-type']); // 'image/png'
626 });
627
628 stream.on('end', () => {
629 // debug("Download done !");
630
631 dest.stat(callback);
632 });
633 });
634 });
635 });
636 });
637 }
638}
639
640
641module.exports = TmdbAPI;