node-apiless-youtube-upload-nc
Version:
Upload videos to Youtube in Node.js without any Youtube API dependency by using Selenium.
197 lines (174 loc) • 9.43 kB
text/typescript
import {until, By, WebElement} from 'selenium-webdriver'
import fs from 'fs'
import {Cookies, makeWebDriver} from '../helpers'
const GOOGLE_URL = `https://google.com`;
const YOUTUBE_STUDIO_URL = `https://studio.youtube.com`;
export default async (vid : string, cookies : Cookies, headlessMode = true, onProgress = console.log):Promise<string> => {
if(!vid) throw new Error("vid 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]
}
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 url = (await driver.getCurrentUrl())
if (!url.includes('studio.youtube.com/')) {
throw new Error(`Cookies are expired or not valid. (tried to upload, was redirected to ${url}`)
}
onProgress('find video...');
//hack:hide tp-yt-iron-overlay-backdrop
var backdrops = await driver.findElements(By.css("tp-yt-iron-overlay-backdrop"));
if(backdrops){
for(var i = 0; i < backdrops.length; i++){
await driver.executeScript('arguments[0].style.height="0px";', backdrops[i]);
}
}
await (await findElement('[id=menu-paper-icon-item-1]')).click()
await driver.sleep(10*1000);
var videoRow = null;
await driver.executeScript('arguments[0].click()', await findElement('#chip-bar .text-input'));
await driver.sleep(1000);
await (await findElement(`[test-id='{"filterName":"VISIBILITY"}']`)).click();
await driver.sleep(1000);
await (await findElement('[test-id=PRIVATE]')).click();
await driver.sleep(1000);
await (await findElement('[test-id=DRAFT]')).click();
await driver.sleep(1000);
await (await findElement('#apply-button')).click();
await driver.sleep(10*1000);
var videoRows = (await findElements('#main [id=row-container]'));
for(var i = 0; i < videoRows.length; i++){
var anchors = await videoRows[i].findElements(By.css('[id=thumbnail-anchor]'));
if(!anchors.length){
anchors = await videoRows[i].findElements(By.css('[id=anchor-watch-on-yt]'));
}
if(anchors.length&&(await anchors[0].getAttribute('href')).indexOf(vid)!=-1){
await driver.executeScript('arguments[0].scrollIntoViewIfNeeded()', videoRows[i]);
videoRow = videoRows[i];
break;
}
}
var ret = '';
if(videoRow){
var restrictElem = (await videoRow.findElements(By.css('.restrictions-list .ytcp-video-row')))[0];
var copyrightTip = (await restrictElem.getAttribute('innerHTML')).replace(/<!--[\s\S]*-->/g, '');
onProgress('限制:'+copyrightTip);
function checkText(val, vals){
for(var v of vals){
if(val.indexOf(v)!=-1){
return true;
}
}
return false;
}
if(checkText(copyrightTip, ['无', '無', '没有', 'None'])){
onProgress('set video status public...');
var statusBtn = (await (await videoRow.findElements(By.css('[role=cell]')))[2].findElements(By.css('.icon-text-edit-triangle-wrap')))[0];
await driver.executeScript(`var evObj = document.createEvent('MouseEvents');evObj.initEvent('mouseover', true, false);arguments[0].dispatchEvent(evObj)`, statusBtn)
await driver.sleep(1000);
await driver.executeScript('arguments[0].click()', statusBtn);
await driver.sleep(1000);
(await findElements('#dialog [name=PUBLIC]'))[0].click();
await driver.sleep(1000);
(await findElements('#dialog [id=save-button]'))[0].click();
ret = JSON.stringify({status:1});
}
else if(checkText(copyrightTip, ['版权主张', '版權聲明', 'Copyright claim'])){
onProgress('get copyinfo detail...');
await driver.executeScript(`var evObj = document.createEvent('MouseEvents');evObj.initEvent('mouseover', true, false);arguments[0].dispatchEvent(evObj)`, restrictElem);
await driver.sleep(1000);
await driver.executeScript(`var evObj = document.createEvent('MouseEvents');evObj.initEvent('mouseenter', true, false);arguments[0].dispatchEvent(evObj)`, restrictElem);
await driver.sleep(1000);
(await findElement('#tooltip ytcp-button')).click();
await driver.sleep(10*1000);
var rows = (await findElements('#dialog ytcr-claim-row'));
var copyinfo = [];
for(var i = 0; i < rows.length; i++){
await rows[i].click();
await driver.sleep(1000);
copyinfo.push(await (await rows[i].findElements(By.css('button.ytcr-claim-list-cell-match-segments')))[0].getText());
}
if(copyinfo.length){
onProgress('delete video...');
(await findElements('#dialog .ytcr-copyright-section ytcp-icon-button.close-button'))[0].click();
await driver.sleep(1000);
await driver.executeScript(`var evObj = document.createEvent('MouseEvents');evObj.initEvent('mouseover', true, false);arguments[0].dispatchEvent(evObj)`, videoRow);
await driver.sleep(1000);
await (await videoRow.findElements(By.css('ytcp-icon-button.open-menu-button')))[0].click();
await driver.sleep(1000);
(await findElements('#dialog [test-id=delete]'))[0].click();
await driver.sleep(1000);
(await findElements('#delete-dialog [id=checkbox-container]'))[0].click();
await driver.sleep(1000);
(await findElements('#delete-dialog [id=confirm-button]'))[0].click();
ret = JSON.stringify({state:1, copyinfo:JSON.stringify(copyinfo), cropstate:0});
}
else{
throw new Error('not find copyright info!');
}
}
else{
onProgress('nothing to do...');
ret = JSON.stringify({status:2});
}
}
else{
throw new Error('not find a video!');
}
await driver.sleep(2*1000);
return Promise.resolve(ret);
}
catch(e){
return Promise.resolve(e);
}
finally {
if (securityIgnoreInterval) clearInterval(securityIgnoreInterval)
await driver.quit();
}
}