duplicate saftey
[KisSync.git] / src / get-info.js
blobbe79ddfb890e49e1664407c7dc6e75683e6762a1
1 const https = require("https");
2 const Media = require("./media");
3 const CustomEmbedFilter = require("./customembed").filter;
4 const Config = require("./config");
5 const ffmpeg = require("./ffmpeg");
6 const mediaquery = require("@cytube/mediaquery");
7 const YouTube = require("@cytube/mediaquery/lib/provider/youtube");
8 const Vimeo = require("@cytube/mediaquery/lib/provider/vimeo");
9 const Streamable = require("@cytube/mediaquery/lib/provider/streamable");
10 const TwitchVOD = require("@cytube/mediaquery/lib/provider/twitch-vod");
11 const TwitchClip = require("@cytube/mediaquery/lib/provider/twitch-clip");
12 import { Counter } from 'prom-client';
13 import { lookup as lookupCustomMetadata } from './custom-media';
15 const LOGGER = require('@calzoneman/jsli')('get-info');
16 const lookupCounter = new Counter({
17     name: 'cytube_media_lookups_total',
18     help: 'Count of media lookups',
19     labelNames: ['shortCode']
20 });
22 var urlRetrieve = function (transport, options, callback) {
23     var req = transport.request(options, function (res) {
24         res.on("error", function (err) {
25             LOGGER.error("HTTP response " + options.host + options.path + " failed: "+
26                 err);
27             callback(503, "");
28         });
30         var buffer = "";
31         res.setEncoding("utf-8");
32         res.on("data", function (chunk) {
33             buffer += chunk;
34         });
35         res.on("end", function () {
36             callback(res.statusCode, buffer);
37         });
38     });
40     req.on("error", function (err) {
41         LOGGER.error("HTTP request " + options.host + options.path + " failed: " +
42             err);
43         callback(503, "");
44     });
46     req.end();
49 var mediaTypeMap = {
50     "youtube": "yt",
51     "googledrive": "gd",
52     "google+": "gp"
55 function convertMedia(media) {
56     return new Media(media.id, media.title, media.duration, mediaTypeMap[media.type],
57             media.meta);
60 var Getters = {
61     /* youtube.com */
62     yt: function (id, callback) {
63         if (!Config.get("youtube-v3-key")) {
64             return callback("The YouTube API now requires an API key.  Please see the " +
65                             "documentation for youtube-v3-key in config.template.yaml");
66         }
69         YouTube.lookup(id).then(function (video) {
70             var meta = {};
71             if (video.meta.blocked) {
72                 meta.restricted = video.meta.blocked;
73             }
74             if (video.meta.ytRating) {
75                 meta.ytRating = video.meta.ytRating;
76             }
78             var media = new Media(video.id, video.title, video.duration, "yt", meta);
79             callback(false, media);
80         }).catch(function (err) {
81             callback(err.message || err, null);
82         });
83     },
85     /* youtube.com playlists */
86     yp: function (id, callback) {
87         if (!Config.get("youtube-v3-key")) {
88             return callback("The YouTube API now requires an API key.  Please see the " +
89                             "documentation for youtube-v3-key in config.template.yaml");
90         }
92         YouTube.lookupPlaylist(id).then(function (videos) {
93             videos = videos.map(function (video) {
94                 var meta = {};
95                 if (video.meta.blocked) {
96                     meta.restricted = video.meta.blocked;
97                 }
99                 return new Media(video.id, video.title, video.duration, "yt", meta);
100             });
102             callback(null, videos);
103         }).catch(function (err) {
104             callback(err.message || err, null);
105         });
106     },
108     /* youtube.com search */
109     ytSearch: function (query, callback) {
110         if (!Config.get("youtube-v3-key")) {
111             return callback("The YouTube API now requires an API key.  Please see the " +
112                             "documentation for youtube-v3-key in config.template.yaml");
113         }
115         YouTube.search(query).then(function (res) {
116             var videos = res.results;
117             videos = videos.map(function (video) {
118                 var meta = {};
119                 if (video.meta.blocked) {
120                     meta.restricted = video.meta.blocked;
121                 }
123                 var media = new Media(video.id, video.title, video.duration, "yt", meta);
124                 media.thumb = { url: video.meta.thumbnail };
125                 return media;
126             });
128             callback(null, videos);
129         }).catch(function (err) {
130             callback(err.message || err, null);
131         });
132     },
134     /* vimeo.com */
135     vi: function (id, callback) {
136         var m = id.match(/([\w-]+)/);
137         if (m) {
138             id = m[1];
139         } else {
140             callback("Invalid ID", null);
141             return;
142         }
144         Vimeo.lookup(id).then(video => {
145             video = new Media(video.id, video.title, video.duration, "vi");
146             callback(null, video);
147         }).catch(error => {
148             callback(error.message);
149         });
150     },
152     /* dailymotion.com */
153     dm: function (id, callback) {
154         var m = id.match(/([\w-]+)/);
155         if (m) {
156             id = m[1].split("_")[0];
157         } else {
158             callback("Invalid ID", null);
159             return;
160         }
161         var options = {
162             host: "api.dailymotion.com",
163             port: 443,
164             path: "/video/" + id + "?fields=duration,title",
165             method: "GET",
166             dataType: "jsonp",
167             timeout: 1000
168         };
170         urlRetrieve(https, options, function (status, data) {
171             switch (status) {
172                 case 200:
173                     break; /* Request is OK, skip to handling data */
174                 case 400:
175                     return callback("Invalid request", null);
176                 case 403:
177                     return callback("Private video", null);
178                 case 404:
179                     return callback("Video not found", null);
180                 case 500:
181                 case 503:
182                     return callback("Service unavailable", null);
183                 default:
184                     return callback("HTTP " + status, null);
185             }
187             try {
188                 data = JSON.parse(data);
189                 var title = data.title;
190                 var seconds = data.duration;
191                 /**
192                  * This is a rather hacky way to indicate that a video has
193                  * been deleted...
194                  */
195                 if (title === "Deleted video" && seconds === 10) {
196                     callback("Video not found", null);
197                     return;
198                 }
199                 var media = new Media(id, title, seconds, "dm");
200                 callback(false, media);
201             } catch(e) {
202                 callback(e, null);
203             }
204         });
205     },
207     /* soundcloud.com - see https://github.com/calzoneman/sync/issues/916 */
208     sc: function (id, callback) {
209         callback(
210             "Soundcloud is not supported anymore due to requiring OAuth but not " +
211             "accepting new API key registrations."
212         );
213     },
215     /* livestream.com */
216     li: function (id, callback) {
217         var m = id.match(/([\w-]+)/);
218         if (m) {
219             id = m[1];
220         } else {
221             callback("Invalid ID", null);
222             return;
223         }
224         var title = "Livestream.com - " + id;
225         var media = new Media(id, title, "--:--", "li");
226         callback(false, media);
227     },
229     /* twitch.tv */
230     tw: function (id, callback) {
231         var m = id.match(/([\w-]+)/);
232         if (m) {
233             id = m[1];
234         } else {
235             callback("Invalid ID", null);
236             return;
237         }
238         var title = "Twitch.tv - " + id;
239         var media = new Media(id, title, "--:--", "tw");
240         callback(false, media);
241     },
243     /* twitch VOD */
244     tv: function (id, callback) {
245         var m = id.match(/([cv]\d+)/);
246         if (m) {
247             id = m[1];
248         } else {
249             process.nextTick(callback, "Invalid Twitch VOD ID");
250             return;
251         }
253         TwitchVOD.lookup(id).then(video => {
254             const media = new Media(video.id, video.title, video.duration,
255                                     "tv", video.meta);
256             process.nextTick(callback, false, media);
257         }).catch(function (err) {
258             callback(err.message || err, null);
259         });
260     },
262     /* twitch clip */
263     tc: function (id, callback) {
264         var m = id.match(/^([A-Za-z]+)$/);
265         if (m) {
266             id = m[1];
267         } else {
268             process.nextTick(callback, "Invalid Twitch VOD ID");
269             return;
270         }
272         TwitchClip.lookup(id).then(video => {
273             const media = new Media(video.id, video.title, video.duration,
274                                     "tc", video.meta);
275             process.nextTick(callback, false, media);
276         }).catch(function (err) {
277             callback(err.message || err, null);
278         });
279     },
281     /* ustream.tv */
282     us: function (id, callback) {
283         var m = id.match(/(channel\/[^?&#]+)/);
284         if (m) {
285             id = m[1];
286         } else {
287             callback("Invalid ID", null);
288             return;
289         }
291         var options = {
292             host: "www.ustream.tv",
293             port: 443,
294             path: "/" + id,
295             method: "GET",
296             timeout: 1000
297         };
299         urlRetrieve(https, options, function (status, data) {
300             if(status !== 200) {
301                 callback("Ustream HTTP " + status, null);
302                 return;
303             }
305             /*
306              * Yes, regexing this information out of the HTML sucks.
307              * No, there is not a better solution -- it seems IBM
308              * deprecated the old API (or at least replaced with an
309              * enterprise API marked "Contact sales") so fuck it.
310              */
311             var m = data.match(/https:\/\/www\.ustream\.tv\/embed\/(\d+)/);
312             if (m) {
313                 var title = "Ustream.tv - " + id;
314                 var media = new Media(m[1], title, "--:--", "us");
315                 callback(false, media);
316             } else {
317                 callback("Channel ID not found", null);
318             }
319         });
320     },
322     /* rtmp stream */
323     rt: function (id, callback) {
324         var title = "Livestream";
325         var media = new Media(id, title, "--:--", "rt");
326         callback(false, media);
327     },
329     /* HLS stream */
330     hl: function (id, callback) {
331         if (!/^https/.test(id)) {
332             callback(
333                 "HLS links must start with HTTPS due to browser security " +
334                 "policy.  See https://git.io/vpDLK for details."
335             );
336             return;
337         }
338         var title = "Livestream";
339         var media = new Media(id, title, "--:--", "hl");
340         callback(false, media);
341     },
343     /* custom embed */
344     cu: function (id, callback) {
345         var media;
346         try {
347             media = CustomEmbedFilter(id);
348         } catch (e) {
349             if (/invalid embed/i.test(e.message)) {
350                 return callback(e.message);
351             } else {
352                 LOGGER.error(e.stack);
353                 return callback("Unknown error processing embed");
354             }
355         }
356         callback(false, media);
357     },
359     /* google docs */
360     gd: function (id, callback) {
361         if (!/^[a-zA-Z0-9_-]+$/.test(id)) {
362             callback("Invalid ID: " + id);
363             return;
364         }
366         var data = {
367             type: "googledrive",
368             kind: "single",
369             id: id
370         };
372         mediaquery.lookup(data).then(function (video) {
373             callback(null, convertMedia(video));
374         }).catch(function (err) {
375             callback(err.message || err);
376         });
377     },
379     /* ffmpeg for raw files */
380     fi: function (id, cb) {
381         ffmpeg.query(id, function (err, data) {
382             if (err) {
383                 return cb(err);
384             }
386             var m = new Media(id, data.title, data.duration, "fi", {
387                 bitrate: data.bitrate,
388                 codec: data.codec
389             });
390             cb(null, m);
391         });
392     },
394     /* hitbox.tv / smashcast.tv */
395     hb: function (id, callback) {
396         var m = id.match(/([\w-]+)/);
397         if (m) {
398             id = m[1];
399         } else {
400             callback("Invalid ID", null);
401             return;
402         }
403         var title = "Smashcast - " + id;
404         var media = new Media(id, title, "--:--", "hb");
405         callback(false, media);
406     },
408     /* vid.me */
409     vm: function (id, callback) {
410         process.nextTick(
411             callback,
412             "As of December 2017, vid.me is no longer in service."
413         );
414     },
416     /* streamable */
417     sb: function (id, callback) {
418         if (!/^[\w-]+$/.test(id)) {
419             process.nextTick(callback, "Invalid streamable.com ID");
420             return;
421         }
423         Streamable.lookup(id).then(video => {
424             const media = new Media(video.id, video.title, video.duration,
425                                     "sb", video.meta);
426             process.nextTick(callback, false, media);
427         }).catch(function (err) {
428             callback(err.message || err, null);
429         });
430     },
432     /* custom media - https://github.com/calzoneman/sync/issues/655 */
433     cm: async function (id, callback) {
434         try {
435             const media = await lookupCustomMetadata(id);
436             process.nextTick(callback, false, media);
437         } catch (error) {
438             process.nextTick(callback, error.message);
439         }
440     },
442     /* mixer.com */
443     mx: function (id, callback) {
444         process.nextTick(
445             callback,
446             "As of July 2020, Mixer is no longer in service."
447         );
448     }
451 module.exports = {
452     Getters: Getters,
453     getMedia: function (id, type, callback) {
454         if(type in this.Getters) {
455             LOGGER.info("Looking up %s:%s", type, id);
456             lookupCounter.labels(type).inc(1, new Date());
457             this.Getters[type](id, callback);
458         } else {
459             callback("Unknown media type '" + type + "'", null);
460         }
461     }