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 interface VideoObj { videoPath : string ; title : string ; thumbnailPath ? : string ; description ? : string ; monetization : boolean ; visibility ? : 'private' | 'unlisted' | 'public' ; } const validateVideoObj = ( videoObj ) => { if ( ! videoObj . videoPath ) throw new Error ( "VideoObj: missing required property videoPath" ) ; if ( ! videoObj . title ) throw new Error ( "VideoObj: missing required property title" ) ; if ( videoObj . title . length > 100 ) throw new Error ( "VideoObj: given title is longer than max 100 characters" ) ; if ( videoObj . description . length > 5000 ) throw new Error ( "VideoObj: given description is longer than max 5000 characters" ) ; if ( videoObj . visibility && ! [ 'private' , 'unlisted' , 'public' ] . includes ( videoObj . visibility ) ) throw new Error ( `VideoObj: given visibility value "${ videoObj . visibility }" is not valid. Should be private, unlisted or public` ) ; if ( ! fs . existsSync ( videoObj . videoPath ) ) throw new Error ( `VideoObj: given videoPath doesn't exist on disk (${ videoObj . videoPath })` ) ; if ( videoObj . thumbnailPath && ! fs . existsSync ( videoObj . thumbnailPath ) ) throw new Error ( `VideoObj: given thumbnailPath doesn't exist on disk (${ videoObj . thumbnailPath })` ) ; } ; export default async ( videoObj , cookies , headlessMode = true , onProgress = console . log ) => { if ( ! cookies || ! cookies . length ) throw new Error ( "Can't upload video: cookies not set." ) ; validateVideoObj ( videoObj ) ; videoObj = { visibility : 'public' , monetization : false , description : '' , ... videoObj } ; const driver = await makeWebDriver ( { headless : headlessMode , fullsize : true } ) ; const enterEmojiString = async ( webElement , string ) => { 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 ) => { 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 ) => { 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 ) => { var els = await findElements ( cssSelector ) ; if ( els . length == 0 ) return false ; return els [ 0 ] ; } ; const tryMonetization = async ( ) => { const monetizationTabButton = await tryFindElement ( 'button[test-id=MONETIZATION]' ) ; if ( ! monetizationTabButton ) return onProgress ( 'Monetization options are not available on this channel. Continuing..' ) ; onProgress ( "Applying monetization settings.." ) ; await monetizationTabButton . click ( ) ; await driver . sleep ( 500 ) ; await ( await findElement ( 'ytcp-icon-button[class~=ytcp-video-monetization]' ) ) . click ( ) ; await driver . sleep ( 500 ) ; var isAlreadyOff = ( await ( await findElement ( 'paper-radio-button[id=radio-off][class~=ytcp-video-monetization-edit-dialog]' ) ) . getAttribute ( 'aria-selected' ) ) === "true" ? true : false ; if ( isAlreadyOff && videoObj . monetization ) { onProgress ( 'Setting monetization on...' ) ; await ( await findElement ( 'paper-radio-button[id=radio-on][class~=ytcp-video-monetization-edit-dialog]' ) ) . click ( ) ; await driver . sleep ( 500 ) ; await ( await findElement ( 'ytcp-button[id=save-button][class~=ytcp-video-monetization-edit-dialog]' ) ) . click ( ) ; } else if ( ! isAlreadyOff && ! videoObj . monetization ) { onProgress ( 'Setting monetization off...' ) ; await ( await findElement ( 'paper-radio-button[id=radio-off][class~=ytcp-video-monetization-edit-dialog]' ) ) . click ( ) ; await driver . sleep ( 500 ) ; await ( await findElement ( 'ytcp-button[id=save-button][class~=ytcp-video-monetization-edit-dialog]' ) ) . click ( ) ; } else { onProgress ( `Monetization is already the desired value (${ isAlreadyOff ? 'off' : 'on' })...` ) ; await ( await findElement ( 'iron-overlay-backdrop[opened]' ) ) . click ( ) ; } await driver . sleep ( 500 ) ; } ; var securityIgnoreInterval = null ; var successInterval = null ; try { onProgress ( 'Settings cookies..' ) ; await driver . get ( GOOGLE_URL ) ; for ( let cookie of cookies ) await driver . manage ( ) . addCookie ( cookie ) ; onProgress ( 'Opening Youtube Studio..' ) ; await driver . get ( YOUTUBE_STUDIO_URL ) ; await driver . sleep ( 1000 ) ; securityIgnoreInterval = setInterval ( ( ) => { ensureNoSecurityWarning ( ) ; } , 500 ) ; 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 }` ) ; } await ( await findElement ( "#upload-icon > .remove-defaults" ) ) . click ( ) ; await driver . wait ( until . elementsLocated ( By . css ( "input[type=file]" ) ) , 10000 ) ; onProgress ( 'Initializing video..' ) ; await ( await findElement ( "input[type=file]" ) ) . sendKeys ( videoObj . videoPath ) ; await driver . wait ( until . elementsLocated ( By . css ( "#textbox" ) ) , 10000 ) ; await driver . sleep ( 10000 ) ; var editBoxes = await findElements ( "#textbox" ) ; onProgress ( 'Initializing title and description..' ) ; var titleBox = editBoxes [ 0 ] ; var descriptionBox = editBoxes [ 1 ] ; await enterEmojiString ( titleBox , videoObj . title ) ; await enterEmojiString ( descriptionBox , videoObj . description ) ; await driver . sleep ( 1000 ) ; onProgress ( 'Confirming title and description..' ) ; await enterEmojiString ( titleBox , videoObj . title ) ; await enterEmojiString ( descriptionBox , videoObj . description ) ; onProgress ( 'Entering custom thumbnail..' ) ; if ( videoObj . thumbnailPath ) { var thumbnailBox = await findElement ( "#file-loader" ) ; await thumbnailBox . sendKeys ( videoObj . thumbnailPath ) ; } await driver . sleep ( 5000 ) ; onProgress ( 'Setting "not made for kids" (the only supported options right now)..' ) ; await ( await findElement ( '[name=NOT_MADE_FOR_KIDS]' ) ) . click ( ) ; await driver . sleep ( 1000 ) ; await tryMonetization ( ) ; onProgress ( `Setting visibility option to ${ videoObj . visibility }..` ) ; await ( await findElement ( 'button[test-id=REVIEW]' ) ) . click ( ) ; await driver . wait ( until . elementsLocated ( By . css ( "#privacy-radios" ) ) , 10000 ) ; var [ hiddenButton , unlistedButton , publicButton ] = await findElements ( "#privacy-radios > paper-radio-button" ) ; switch ( videoObj . visibility ) { case 'private' : hiddenButton . click ( ) ; break ; case 'unlisted' : unlistedButton . click ( ) ; break ; case 'public' : publicButton . click ( ) ; break ; default : throw new Error ( "Unrecognized visibility option" ) ; } onProgress ( 'Uploading..' ) ; await new Promise ( ( resolve , reject ) => { successInterval = setInterval ( async ( ) => { try { var progressEl = await findElement ( "ytcp-video-upload-progress > .progress-label" ) ; var innerHTML = ( await progressEl . getText ( ) ) . replace ( /&nbsp/g , ' ' ) ; onProgress ( innerHTML ) ; if ( /\D \.\.\. \D/ . test ( innerHTML ) || /^[^\.]+$/ . test ( innerHTML ) ) { clearInterval ( successInterval ) ; onProgress ( "Publishing.." ) ; await ( await findElement ( "#done-button" ) ) . click ( ) ; await driver . sleep ( 2000 ) ; var confirmationMaybe = await tryFindElement ( 'ytcp-button[id=publish-button][class~=ytcp-prechecks-warning-dialog]' ) ; if ( confirmationMaybe ) confirmationMaybe . click ( ) ; await driver . sleep ( 1000 ) ; onProgress ( 'Done! (video may still be processing, but it is uploaded)' ) ; return resolve ( undefined ) ; } } catch ( e ) { reject ( e ) ; } } , 4000 ) ; } ) ; return null ; } finally { if ( securityIgnoreInterval ) clearInterval ( securityIgnoreInterval ) ; if ( successInterval ) clearInterval ( successInterval ) ; await driver . quit ( ) ; } } ;