node-apiless-youtube-upload-nc
Version:
Upload videos to Youtube in Node.js without any Youtube API dependency by using Selenium.
218 lines (192 loc) • 9.22 kB
text/typescript
import {until, By, WebElement} from 'selenium-webdriver'
import {Cookies, makeWebDriver} from '../helpers'
var URL = require('url');
const GOOGLE_URL = `https://google.com`;
const YOUTUBE_STUDIO_URL = `https://studio.youtube.com`;
export default async (url : string, hs : string, cookies : Cookies, vid : string, headlessMode = true, onProgress = console.log):Promise<string> => {
if(!url) throw new Error("url not set.")
if(!hs) throw new Error("hs not set.")
if (!cookies || !cookies.length) throw new Error("Can't check video copyright: cookies not set.")
const driver = await makeWebDriver({headless: headlessMode, fullsize: true})
const enterEmojiString = async (webElement : WebElement, string : string) => {
// sendKeys(string) doesn't support emojis (causes a crash)
// youtube custom input elements don't have "value" property (but webEl.clear() still works)
// clipboard hack works but not in headless mode
// also editing innerHTML causes racing conditions with the underlying javascript mechanism
// solution is to use an obsolete method document.execCommand('insreText')
await driver.sleep(500)
webElement.click()
await driver.sleep(250)
webElement.clear()
await driver.sleep(250)
await driver.executeScript(`
arguments[0].focus();
document.execCommand('insertText', false, arguments[1]);
`, webElement, string)
}
const ensureNoSecurityWarning = async () => {
await driver.executeScript("if (document.querySelector('ytcp-auth-confirmation-dialog')) document.querySelector('ytcp-auth-confirmation-dialog').remove()")
}
const findElements = async (cssSelector : string) => {
var webEls = await driver.findElements(By.css(cssSelector))
if (webEls[0]) await driver.executeScript('arguments[0].scrollIntoViewIfNeeded()', webEls[0])
return webEls
}
const findElement = async (cssSelector : string) => {
var els = await findElements(cssSelector)
if (els.length === 0) throw new Error(`Element was not found with selector '${cssSelector}'`)
return els[0]
}
const tryFindElement = async (cssSelector : string) => {
var els = await findElements(cssSelector)
if (els.length == 0) return false
return els[0]
}
function getRandom(max, min){
if(min!==undefined&&max!=min){
return Math.floor(min + (max -min) *Math.random());
}
else{
return Math.floor(max*Math.random());
}
}
var securityIgnoreInterval = null
try {
onProgress('Settings cookies..')
// Load google page to set up cookies
await driver.get(GOOGLE_URL)
// Add cookies
for (let cookie of cookies) await driver.manage().addCookie(cookie)
onProgress('Opening Youtube Studio..')
// Open Youtube Studio page
await driver.get(YOUTUBE_STUDIO_URL);
// Wait for stuff to fully load
await driver.sleep(1000)
securityIgnoreInterval = setInterval(() => {
ensureNoSecurityWarning()
}, 500)
// Check if url is still studio.youtube.com and not accounts.google.com (which is the case if cookies are not valid / are expired)
var currentUrl = (await driver.getCurrentUrl())
if (!currentUrl.includes('studio.youtube.com/')) {
throw new Error(`Cookies are expired or not valid. (tried to upload, was redirected to ${currentUrl}`)
}
// Open url
if(!url.endsWith('/videos')){
url += '/videos';
}
onProgress('Opening ' + url)
await driver.get(url);
await driver.sleep(1000);
var items = (await findElements('#items ytd-grid-video-renderer a[id=thumbnail]'));
var vids = [];
for(var i = 0; i < items.length; i++){
var params = URL.parse(await items[i].getAttribute('href')).search.substr(1).split('&');
for(var j = 0; j < params.length; j++){
var pair = params[j].split('=');
if(pair[0]=='v'){
vids.push(pair[1]);
break;
}
}
}
if(vids.length){
var sn = -1;
for(var i = 0; i < vids.length; i++){
if(vids[i]==vid){
sn = i;
break;
}
}
if(sn==0){
console.log(`latest video has been remarked!`);
return Promise.resolve(vid);
}
else {
vid = vids[0];
if (sn != -1) {
vid = vids[sn - 1];
}
if(Math.random()<0.9){
var url = 'https://www.youtube.com/watch?v=' + vid;
onProgress('Opening ' + url);
await driver.get(url);
await driver.sleep(getRandom(5, 2)*1000);
var metaEl = await findElement('[id=primary-inner] [id=meta]');
var location = await metaEl.getRect();
onProgress('click like button..');
var likeCnt = getRandom(3, 1);
for(var i = 0; i < likeCnt; i++){
await driver.executeScript('window.scrollTo(0, arguments[0])', location.y+location.height+300*i);
await driver.sleep(getRandom(5, 2)*1000);
var likeBtns = await findElements('ytd-toggle-button-renderer[id=like-button]');
if(likeBtns.length){
var likeBtn = likeBtns[getRandom(likeBtns.length, 0)];
await driver.executeScript('arguments[0].scrollIntoViewIfNeeded()', likeBtn);
if((await likeBtn.getAttribute('class')).indexOf('style-default-active')==-1){
try{
await likeBtn.click();
}
catch(e){
console.error('error occur when click like button!');
console.error(e);
}
}
}
await driver.sleep(getRandom(2, 1)*1000);
await driver.executeScript('window.scrollTo(0, 0)');
await driver.sleep(getRandom(2, 1)*1000);
}
onProgress('add comment..');
await driver.executeScript('window.scrollTo(0, arguments[0])', location.y+location.height);
await driver.sleep(getRandom(5, 2) * 1000);
onProgress('remark..');
(await findElement('#simplebox-placeholder')).click()
await driver.sleep(1000);
var cts = await findElements('yt-formatted-string[id=content-text]');
if(cts.length){
var ct = cts[getRandom(cts.length, 0)];
await driver.executeScript('arguments[0].scrollIntoViewIfNeeded()', ct);
try{
var curText = await ct.getText();
var rVal = Math.random();
if(rVal<0.15){
hs = curText + ' ' + hs;
}
else if(rVal<0.3){
hs = hs + ' ' + curText;
}
else if(rVal<0.4){
hs = curText;
}
}
catch(e){
console.error('error occur when get text!');
console.error(e);
}
}
var inputElem = await findElement('[id=contenteditable-root]');
await driver.executeScript('arguments[0].scrollIntoViewIfNeeded()', inputElem);
await inputElem.sendKeys(hs);
await driver.sleep(getRandom(5, 2)*1000);
(await findElement('#submit-button [id=button]')).click();
await driver.sleep(getRandom(5, 2)*1000);
}
else{
console.log('ignore comments!');
}
return Promise.resolve(vid);
}
}
else{
console.log(`videos is empty!`);
return Promise.resolve('');
}
}
catch(e){
return Promise.resolve('');
}
finally {
if (securityIgnoreInterval) clearInterval(securityIgnoreInterval)
await driver.quit();
}
}