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']
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: "+
31 res.setEncoding("utf-8");
32 res.on("data", function (chunk) {
35 res.on("end", function () {
36 callback(res.statusCode, buffer);
40 req.on("error", function (err) {
41 LOGGER.error("HTTP request " + options.host + options.path + " failed: " +
55 function convertMedia(media) {
56 return new Media(media.id, media.title, media.duration, mediaTypeMap[media.type],
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");
69 YouTube.lookup(id).then(function (video) {
71 if (video.meta.blocked) {
72 meta.restricted = video.meta.blocked;
74 if (video.meta.ytRating) {
75 meta.ytRating = video.meta.ytRating;
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);
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");
92 YouTube.lookupPlaylist(id).then(function (videos) {
93 videos = videos.map(function (video) {
95 if (video.meta.blocked) {
96 meta.restricted = video.meta.blocked;
99 return new Media(video.id, video.title, video.duration, "yt", meta);
102 callback(null, videos);
103 }).catch(function (err) {
104 callback(err.message || err, null);
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");
115 YouTube.search(query).then(function (res) {
116 var videos = res.results;
117 videos = videos.map(function (video) {
119 if (video.meta.blocked) {
120 meta.restricted = video.meta.blocked;
123 var media = new Media(video.id, video.title, video.duration, "yt", meta);
124 media.thumb = { url: video.meta.thumbnail };
128 callback(null, videos);
129 }).catch(function (err) {
130 callback(err.message || err, null);
135 vi: function (id, callback) {
136 var m = id.match(/([\w-]+)/);
140 callback("Invalid ID", null);
144 Vimeo.lookup(id).then(video => {
145 video = new Media(video.id, video.title, video.duration, "vi");
146 callback(null, video);
148 callback(error.message);
152 /* dailymotion.com */
153 dm: function (id, callback) {
154 var m = id.match(/([\w-]+)/);
156 id = m[1].split("_")[0];
158 callback("Invalid ID", null);
162 host: "api.dailymotion.com",
164 path: "/video/" + id + "?fields=duration,title",
170 urlRetrieve(https, options, function (status, data) {
173 break; /* Request is OK, skip to handling data */
175 return callback("Invalid request", null);
177 return callback("Private video", null);
179 return callback("Video not found", null);
182 return callback("Service unavailable", null);
184 return callback("HTTP " + status, null);
188 data = JSON.parse(data);
189 var title = data.title;
190 var seconds = data.duration;
192 * This is a rather hacky way to indicate that a video has
195 if (title === "Deleted video" && seconds === 10) {
196 callback("Video not found", null);
199 var media = new Media(id, title, seconds, "dm");
200 callback(false, media);
207 /* soundcloud.com - see https://github.com/calzoneman/sync/issues/916 */
208 sc: function (id, callback) {
210 "Soundcloud is not supported anymore due to requiring OAuth but not " +
211 "accepting new API key registrations."
216 li: function (id, callback) {
217 var m = id.match(/([\w-]+)/);
221 callback("Invalid ID", null);
224 var title = "Livestream.com - " + id;
225 var media = new Media(id, title, "--:--", "li");
226 callback(false, media);
230 tw: function (id, callback) {
231 var m = id.match(/([\w-]+)/);
235 callback("Invalid ID", null);
238 var title = "Twitch.tv - " + id;
239 var media = new Media(id, title, "--:--", "tw");
240 callback(false, media);
244 tv: function (id, callback) {
245 var m = id.match(/([cv]\d+)/);
249 process.nextTick(callback, "Invalid Twitch VOD ID");
253 TwitchVOD.lookup(id).then(video => {
254 const media = new Media(video.id, video.title, video.duration,
256 process.nextTick(callback, false, media);
257 }).catch(function (err) {
258 callback(err.message || err, null);
263 tc: function (id, callback) {
264 var m = id.match(/^([A-Za-z]+)$/);
268 process.nextTick(callback, "Invalid Twitch VOD ID");
272 TwitchClip.lookup(id).then(video => {
273 const media = new Media(video.id, video.title, video.duration,
275 process.nextTick(callback, false, media);
276 }).catch(function (err) {
277 callback(err.message || err, null);
282 us: function (id, callback) {
283 var m = id.match(/(channel\/[^?&#]+)/);
287 callback("Invalid ID", null);
292 host: "www.ustream.tv",
299 urlRetrieve(https, options, function (status, data) {
301 callback("Ustream HTTP " + status, null);
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.
311 var m = data.match(/https:\/\/www\.ustream\.tv\/embed\/(\d+)/);
313 var title = "Ustream.tv - " + id;
314 var media = new Media(m[1], title, "--:--", "us");
315 callback(false, media);
317 callback("Channel ID not found", null);
323 rt: function (id, callback) {
324 var title = "Livestream";
325 var media = new Media(id, title, "--:--", "rt");
326 callback(false, media);
330 hl: function (id, callback) {
331 if (!/^https/.test(id)) {
333 "HLS links must start with HTTPS due to browser security " +
334 "policy. See https://git.io/vpDLK for details."
338 var title = "Livestream";
339 var media = new Media(id, title, "--:--", "hl");
340 callback(false, media);
344 cu: function (id, callback) {
347 media = CustomEmbedFilter(id);
349 if (/invalid embed/i.test(e.message)) {
350 return callback(e.message);
352 LOGGER.error(e.stack);
353 return callback("Unknown error processing embed");
356 callback(false, media);
360 gd: function (id, callback) {
361 if (!/^[a-zA-Z0-9_-]+$/.test(id)) {
362 callback("Invalid ID: " + id);
372 mediaquery.lookup(data).then(function (video) {
373 callback(null, convertMedia(video));
374 }).catch(function (err) {
375 callback(err.message || err);
379 /* ffmpeg for raw files */
380 fi: function (id, cb) {
381 ffmpeg.query(id, function (err, data) {
386 var m = new Media(id, data.title, data.duration, "fi", {
387 bitrate: data.bitrate,
394 /* hitbox.tv / smashcast.tv */
395 hb: function (id, callback) {
396 var m = id.match(/([\w-]+)/);
400 callback("Invalid ID", null);
403 var title = "Smashcast - " + id;
404 var media = new Media(id, title, "--:--", "hb");
405 callback(false, media);
409 vm: function (id, callback) {
412 "As of December 2017, vid.me is no longer in service."
417 sb: function (id, callback) {
418 if (!/^[\w-]+$/.test(id)) {
419 process.nextTick(callback, "Invalid streamable.com ID");
423 Streamable.lookup(id).then(video => {
424 const media = new Media(video.id, video.title, video.duration,
426 process.nextTick(callback, false, media);
427 }).catch(function (err) {
428 callback(err.message || err, null);
432 /* custom media - https://github.com/calzoneman/sync/issues/655 */
433 cm: async function (id, callback) {
435 const media = await lookupCustomMetadata(id);
436 process.nextTick(callback, false, media);
438 process.nextTick(callback, error.message);
443 mx: function (id, callback) {
446 "As of July 2020, Mixer is no longer in service."
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);
459 callback("Unknown media type '" + type + "'", null);