twitchvod-youtube
Version:
> Download any public vod from Twitch and automatically upload at YouTube <br /> from 144p to 1080p
185 lines (164 loc) • 5.29 kB
JavaScript
const axios = require("axios");
const fs = require("fs");
const path = require("path");
const m3u8Parser = require("m3u8-parser");
const dayjs = require("dayjs");
const youtubeuploader = require('extra-youtubeuploader');
const dayjsDuration = require("dayjs/plugin/duration");
dayjs.extend(dayjsDuration);
function humanizeDuration(duration) {
let str = "";
const hours = duration.hours();
const minutes = duration.minutes();
if (hours) {
str += `${hours}h `;
}
if (minutes) {
str += `${minutes}m`;
} else {
str += ` 00m`;
}
return str;
}
function secondsToHms(d) {
d = Number(d);
var h = Math.floor(d / 3600);
var m = Math.floor(d % 3600 / 60);
var s = Math.floor(d % 3600 % 60);
var hDisplay = h > 0 ? h + (h == 1 ? " hour, " : " hours, ") : "";
var mDisplay = m > 0 ? m + (m == 1 ? " minute, " : " minutes, ") : "";
var sDisplay = s > 0 ? s + (s == 1 ? " second" : " seconds") : "";
return hDisplay + mDisplay + sDisplay;
}
async function downloadVodURI(writer, playlist, p) {
// segments
const playlistM3U = await fetchVodPlaylistM3u8(playlist.uri);
const segments = playlistM3U.segments;
// baseURL
let playlistBaseURL = playlist.uri.split("/");
playlistBaseURL.pop();
playlistBaseURL = playlistBaseURL.join("/");
const seconds = segments.reduce((acc, segment) => {
return acc + segment.duration;
}, 0);
console.log(`
Location: ${path.resolve(process.cwd(), writer.path)}
Quality: ${playlist.attributes.RESOLUTION.height}p (${playlist.attributes.VIDEO})
Duration: ${humanizeDuration(dayjs.duration(seconds * 1000))}
`);
const startIndex = playlistM3U.discontinuityStarts.length
? playlistM3U.discontinuityStarts[0]
: 0;
for (let i = startIndex; i < segments.length; i++) {
const segment = segments[i];
var o = segments.length - i;
var left = secondsToHms(o);
process.stdout.clearLine();
process.stdout.cursorTo(0);
process.stdout.write(
`Progress: ${Math.round((i * 100) / segments.length)}% (${i}/${
segments.length - 1
}) Until the end: ${left}`
);
await downloadChunk(writer, `${playlistBaseURL}/${segment.uri}`);
}
writer.end(() => youtubeuploader(p));
}
async function downloadChunk(w, downloadUrl) {
const response = await axios.request({
method: "GET",
url: downloadUrl,
responseType: "stream",
});
response.data.pipe(w, { end: false });
return new Promise((resolve, reject) => {
response.data.on("end", () => {
resolve();
});
response.data.on("error", (err) => {
console.error(err);
reject(err);
});
});
}
function parseM3U8(m3uText) {
// https://wikipedia.org/wiki/M3U
const parser = new m3u8Parser.Parser();
parser.push(m3uText);
parser.end();
return parser.manifest;
}
async function fetchVodM3u8(vodId, { token, sig } = {}) {
const res = await axios.request({
method: "GET",
url: `https://usher.ttvnw.net/vod/${vodId}.m3u8`,
params: {
token,
sig,
allow_source: 'true',
},
});
return parseM3U8(res.data);
}
async function fetchVodCredentials(vodID) {
const credentialsRes = await axios.request({
method: "POST",
url: "https://gql.twitch.tv/gql",
data: JSON.stringify({
operationName: "PlaybackAccessToken_Template",
query:
'query PlaybackAccessToken_Template($login: String!, $isLive: Boolean!, $vodID: ID!, $isVod: Boolean!, $playerType: String!) { streamPlaybackAccessToken(channelName: $login, params: {platform: "web", playerBackend: "mediaplayer", playerType: $playerType}) @include(if: $isLive) { value signature __typename } videoPlaybackAccessToken(id: $vodID, params: {platform: "web", playerBackend: "mediaplayer", playerType: $playerType}) @include(if: $isVod) { value signature __typename }}',
variables: {
isLive: false,
login: "",
isVod: true,
vodID: String(vodID),
playerType: "site",
},
}),
headers: {
"client-id": "kimne78kx3ncx6brgo4mv6wki5h1ko",
},
});
const { videoPlaybackAccessToken } = credentialsRes.data.data;
return {
token: videoPlaybackAccessToken.value,
sig: videoPlaybackAccessToken.signature,
};
}
async function fetchVodPlaylistM3u8(uri) {
const res = await axios.get(uri);
return parseM3U8(res.data);
}
function isValidUrl(string) {
try {
new URL(string);
} catch (_) {
return false;
}
return true;
}
async function downloadTwitchVod(vodIdOrURL, k = {}) {
var o = Object.assign({}, k, {video: `${vodIdOrURL}.mp4`});
if (!vodIdOrURL) {
throw new Error("VOD ID or URL is missing");
}
const vodId = isValidUrl(vodIdOrURL)
? vodIdOrURL.split("/").pop()
: vodIdOrURL;
try {
const vodCredentials = await fetchVodCredentials(vodId);
const manifestVods = await fetchVodM3u8(vodId, vodCredentials);
const bestPlaylist = manifestVods.playlists[0];
const writer = fs.createWriteStream(`${vodId}.mp4`);
await downloadVodURI(writer, bestPlaylist, o);
} catch (err) {
console.error("\nFailed to download VOD:");
if (err.response) {
console.error(err.response.data);
} else {
console.error(err);
}
}
}
module.exports = downloadTwitchVod;