You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
110 lines
3.6 KiB
110 lines
3.6 KiB
var axios = require('axios');
|
|
var M3U = require('playlist-parser').M3U;
|
|
var qs = require('querystring');
|
|
|
|
const clientId = 'kimne78kx3ncx6brgo4mv6wki5h1ko';
|
|
|
|
// Thanks michaelowens, :)
|
|
// Simple titlecase thing, capitalize first letter
|
|
var titleCase = function(str) {
|
|
return str.split(' ').map(function(word) { return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); }).join(' ');
|
|
}
|
|
|
|
|
|
// Twitch functions
|
|
var getAccessToken = function(channel) {
|
|
// Get access token
|
|
return axios.get('https://api.twitch.tv/api/channels/' + channel + '/access_token', {
|
|
headers: {
|
|
'Client-ID': clientId
|
|
}
|
|
}).then(function(res) {
|
|
return res.data;
|
|
});
|
|
}
|
|
|
|
var getPlaylist = function(channel, accessToken) {
|
|
// Get the playlist with given access token data (parsed /access_token response)
|
|
var query = {
|
|
player: 'twitchweb',
|
|
token: accessToken.token,
|
|
sig: accessToken.sig,
|
|
allow_audio_only: 'true',
|
|
allow_source: 'true',
|
|
type: 'any',
|
|
p: Math.floor(Math.random() * 99999) + 1
|
|
};
|
|
|
|
return axios.get('https://usher.ttvnw.net/api/channel/hls/' + channel + '.m3u8?' + qs.stringify(query), {
|
|
headers: {
|
|
'Client-ID': clientId
|
|
}
|
|
}).then(function(res) {
|
|
return res.data;
|
|
});
|
|
}
|
|
|
|
// Exposed functions
|
|
// Just get the playlist, return the string nothing else
|
|
var getPlaylistOnly = function(channel) {
|
|
if (!channel)
|
|
return Promise.reject(new Error('No channel defined.'));
|
|
|
|
var channel = channel.toLowerCase(); // Twitch API only takes lowercase
|
|
return getAccessToken(channel)
|
|
.then(function(token) {
|
|
return getPlaylist(channel, token);
|
|
});
|
|
}
|
|
|
|
// Above get playlist, but then parses it and gives the object
|
|
var getPlaylistParsed = function(channel) {
|
|
if (!channel)
|
|
return Promise.reject(new Error('No channel defined.'));
|
|
|
|
return getPlaylistOnly(channel)
|
|
.then(function(data) {
|
|
// basically parse then _.compact (remove falsy values)
|
|
return M3U.parse(data).filter(function(d) { return d; });
|
|
});
|
|
}
|
|
|
|
var getStreamUrls = function(channel) { // This returns the one with a custom fully parsed object
|
|
return getPlaylistParsed(channel)
|
|
.then(function(playlist) {
|
|
if (playlist.length < 1)
|
|
throw new Error('There were no results, maybe the channel is offline?');
|
|
|
|
// Parse playlist with quality options and send to new array of objects
|
|
var streamLinks = [];
|
|
for (var i = 0; i < playlist.length; i++) {
|
|
// Quality option
|
|
var name = playlist[i].title.match(/VIDEO=('|")(.*?)('|")/); // Raw quality name
|
|
name = name[2]; // Get regex captured group
|
|
|
|
// Rename checks
|
|
// chunked = source
|
|
if (name === 'chunked') name = 'source';
|
|
// audio_only = Audio Only
|
|
else if (name === 'audio_only') name = 'audio only';
|
|
|
|
// Resolution
|
|
var resMatch = playlist[i].title.match(/RESOLUTION=(.*?),/);
|
|
var res = resMatch ? resMatch[1] : null // Audio only does not have a res so we need this check
|
|
|
|
streamLinks.push({
|
|
quality: titleCase(name), // Title case the quality
|
|
resolution: res,
|
|
url: playlist[i].file
|
|
});
|
|
}
|
|
|
|
return streamLinks;
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
get: getStreamUrls,
|
|
raw: getPlaylistOnly,
|
|
rawParsed: getPlaylistParsed
|
|
} |