|
|
@ -5,20 +5,24 @@ const request = require('request'); |
|
|
|
const cheerio = require('cheerio'); |
|
|
|
|
|
|
|
const YOUTUBE_BASE = 'https://www.youtube.com/%s'; |
|
|
|
const WATCH_BASE = util.format(YOUTUBE_BASE, 'watch?v=%s'); |
|
|
|
|
|
|
|
const VIDEO_ID_REGEX = /watch\?v=([^&]*)/i; |
|
|
|
const END_SCREEN_BASE = util.format(YOUTUBE_BASE, 'get_endscreen?v=%s'); |
|
|
|
const CARD_BASE = util.format(YOUTUBE_BASE, 'annotations_invideo?video_id=%s'); |
|
|
|
// const END_SCREEN_BASE = util.format(YOUTUBE_BASE, 'get_endscreen?v=%s');
|
|
|
|
// const CARD_BASE = util.format(YOUTUBE_BASE, 'annotations_invideo?video_id=%s');
|
|
|
|
|
|
|
|
const playerRegex = /ytplayer\.config = (\{.*?\});/; |
|
|
|
|
|
|
|
/** |
|
|
|
* Récupère une liste des vidéos liées à la fin d’une autre vidéo. |
|
|
|
* Fetch the `ytplayer.config` object for a YouTube video. |
|
|
|
* |
|
|
|
* @param videoId Identifiant de la vidéo source. |
|
|
|
* @return Liste des vidéos liées. Chaque vidéo est un objet contenant |
|
|
|
* son identifiant (videoId) et son titre (title). |
|
|
|
* @async |
|
|
|
* @param videoId Identifier of the video to fetch. |
|
|
|
* @return The player configuration object. |
|
|
|
*/ |
|
|
|
const getEndScreenVideos = videoId => |
|
|
|
const getPlayerConfig = videoId => |
|
|
|
{ |
|
|
|
const url = util.format(END_SCREEN_BASE, videoId); |
|
|
|
const url = util.format(WATCH_BASE, videoId); |
|
|
|
|
|
|
|
return new Promise((resolve, reject) => |
|
|
|
{ |
|
|
@ -30,87 +34,84 @@ const getEndScreenVideos = videoId => |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// Suppression des caractères initiaux inutiles si applicable
|
|
|
|
if (body.substr(0, 3) === ')]}') |
|
|
|
// Look for the definition of ytplayer.config and unserialize it
|
|
|
|
// and the player_response subobject
|
|
|
|
try |
|
|
|
{ |
|
|
|
body = body.substr(3); |
|
|
|
const playerConfig = JSON.parse(body.match(playerRegex)[1]); |
|
|
|
playerConfig.args.player_response |
|
|
|
= JSON.parse(playerConfig.args.player_response); |
|
|
|
resolve(playerConfig); |
|
|
|
} |
|
|
|
|
|
|
|
// Interprétation du JSON
|
|
|
|
const data = JSON.parse(body); |
|
|
|
|
|
|
|
// Aucun écran de fin
|
|
|
|
if ( |
|
|
|
typeof data !== 'object' || data === null || |
|
|
|
data.elements === undefined |
|
|
|
) |
|
|
|
catch (err) |
|
|
|
{ |
|
|
|
resolve([]); |
|
|
|
return; |
|
|
|
reject(err); |
|
|
|
} |
|
|
|
|
|
|
|
// Filtrage des données pour extraire le titre et l’ID des vidéos
|
|
|
|
resolve(data.elements.map(elt => |
|
|
|
{ |
|
|
|
const data = elt.endscreenElementRenderer; |
|
|
|
const videoIdResults = VIDEO_ID_REGEX.exec( |
|
|
|
data.endpoint.urlEndpoint.url |
|
|
|
); |
|
|
|
|
|
|
|
return { |
|
|
|
videoId: videoIdResults ? videoIdResults[1] : null, |
|
|
|
title: data.title.simpleText, |
|
|
|
}; |
|
|
|
})); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
exports.getEndScreenVideos = getEndScreenVideos; |
|
|
|
exports.getPlayerConfig = getPlayerConfig; |
|
|
|
|
|
|
|
/** |
|
|
|
* Récupère une liste des vidéos liées en tant que carte d’une autre vidéo. |
|
|
|
* Get metadata about a YouTube video. |
|
|
|
* |
|
|
|
* @param videoId Identifiant de la vidéo source. |
|
|
|
* @return Liste des vidéos liées. Chaque vidéo est un objet contenant |
|
|
|
* son identifiant (videoId) et son titre (title). |
|
|
|
* @param playerConfig The `ytplayer.config` object corresponding to the source |
|
|
|
* YouTube video, as obtained from `getPlayerConfig`. |
|
|
|
* @return Object containing the video metadata. |
|
|
|
*/ |
|
|
|
const getCardVideos = videoId => |
|
|
|
const getVideoMeta = playerConfig => ({ |
|
|
|
videoId: playerConfig.args.player_response.videoDetails.videoId, |
|
|
|
title: playerConfig.args.player_response.videoDetails.title, |
|
|
|
}); |
|
|
|
|
|
|
|
exports.getVideoMeta = getVideoMeta; |
|
|
|
|
|
|
|
/** |
|
|
|
* Find videos linked from the endscreen of a YouTube video. |
|
|
|
* |
|
|
|
* @param playerConfig The `ytplayer.config` object corresponding to the source |
|
|
|
* YouTube video, as obtained from `getPlayerConfig`. |
|
|
|
* @return List of identifiers of linked videos. |
|
|
|
*/ |
|
|
|
const getEndScreenVideos = playerConfig => |
|
|
|
{ |
|
|
|
const url = util.format(CARD_BASE, videoId); |
|
|
|
const response = playerConfig.args.player_response; |
|
|
|
|
|
|
|
return new Promise((resolve, reject) => |
|
|
|
{ |
|
|
|
request(url, (err, res, body) => |
|
|
|
if (!('endscreen' in response)) |
|
|
|
{ |
|
|
|
if (err) |
|
|
|
{ |
|
|
|
reject(err); |
|
|
|
return; |
|
|
|
return []; |
|
|
|
} |
|
|
|
|
|
|
|
// Interprétation du XML externe et recherche des annotations
|
|
|
|
// de type carte
|
|
|
|
const nav = cheerio.load(body); |
|
|
|
const cards = nav('annotation[type="card"][style="video"]'); |
|
|
|
const list = []; |
|
|
|
return response.endscreen.endscreenRenderer.elements |
|
|
|
.map(elt => elt.endscreenElementRenderer) |
|
|
|
.filter(rdr => 'watchEndpoint' in rdr.endpoint) |
|
|
|
.map(rdr => rdr.endpoint.watchEndpoint.videoId); |
|
|
|
}; |
|
|
|
|
|
|
|
cards.each((i, el) => |
|
|
|
exports.getEndScreenVideos = getEndScreenVideos; |
|
|
|
|
|
|
|
/** |
|
|
|
* Find videos linked from as cards from a YouTube video. |
|
|
|
* |
|
|
|
* @param playerConfig The `ytplayer.config` object corresponding to the source |
|
|
|
* YouTube video, as obtained from `getPlayerConfig`. |
|
|
|
* @return List of identifiers of linked videos. |
|
|
|
*/ |
|
|
|
const getCardVideos = playerConfig => |
|
|
|
{ |
|
|
|
const response = playerConfig.args.player_response; |
|
|
|
|
|
|
|
if (!('cards' in response)) |
|
|
|
{ |
|
|
|
// Interprétation du JSON de chaque carte, extraction
|
|
|
|
// du titre et de l’identifiant de la vidéo
|
|
|
|
const data = JSON.parse(nav(el).children('data').text()); |
|
|
|
const videoIdResults = VIDEO_ID_REGEX.exec(data.url); |
|
|
|
|
|
|
|
list.push({ |
|
|
|
title: data.title, |
|
|
|
videoId: videoIdResults ? videoIdResults[1] : null |
|
|
|
}); |
|
|
|
}); |
|
|
|
return []; |
|
|
|
} |
|
|
|
|
|
|
|
resolve(list); |
|
|
|
}); |
|
|
|
}); |
|
|
|
return response.cards.cardCollectionRenderer.cards |
|
|
|
.map(card => card.cardRenderer.content) |
|
|
|
.filter(content => 'videoInfoCardContentRenderer' in content) |
|
|
|
.map(content => content.videoInfoCardContentRenderer) |
|
|
|
.map(rdr => rdr.action.watchEndpoint.videoId); |
|
|
|
}; |
|
|
|
|
|
|
|
exports.getCardVideos = getCardVideos; |
|
|
|