UNPKG

node-apiless-youtube-upload-nc

Version:

Upload videos to Youtube in Node.js without any Youtube API dependency by using Selenium.

156 lines (155 loc) 7.43 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const chrome_location_1 = __importDefault(require("chrome-location")); const Path = __importStar(require("path")); const fs = __importStar(require("fs/promises")); const os = __importStar(require("os")); const child_process_1 = require("child_process"); const helpers_1 = require("../helpers"); const selenium_webdriver_1 = require("selenium-webdriver"); const delayAsync = (ms) => new Promise((res) => setTimeout(res, ms)); const isRunning = (pid) => { try { // @see https://nodejs.org/api/process.html#process_process_kill_pid_signal return process.kill(pid, 0); } catch (error) { return error.code === 'EPERM'; } }; const createTmpDir = async (prefix) => { const path = await fs.mkdtemp(Path.join(os.tmpdir(), prefix)); const remove = () => fs.rm(path, { recursive: true, force: true, maxRetries: 3 }); return { path, remove }; }; // "Chrome didn't shut down correctly" dialog is caused by their // crash handler detecting the process was killed. This might be confusing // to the end user, so we remove it by editing profile preferences const chromePreventCrashDialog = async (profilePath) => { const preferencesPath = Path.join(profilePath, 'Default', 'Preferences'); const jsonBuf = await fs.readFile(preferencesPath); const json = JSON.parse(jsonBuf.toString('utf-8')); json.profile.exit_type = 'Normal'; await fs.writeFile(preferencesPath, JSON.stringify(json)); }; const hasNotLoggedIn = async (pid) => { // The final destination title after a successful login // includes '- YouTube Studio' regardless of language return (0, helpers_1.pid2title)(pid) .then((title) => !title.includes('- YouTube Studio')) .catch((e) => { console.error(e); return true; }); }; const runUncontrolledChrome = async (userDataDir) => { // Spawn a chrome WITHOUT automation features like --remote-debugging-port or --enable automation because they disable google login in most cases // This uncontrolled chrome instance saves cookies to the tempDir. Login status is tracked on windows by process title. const chromeProcess = (0, child_process_1.spawn)(chrome_location_1.default, [ 'https://studio.youtube.com', '--no-first-run', '--no-default-browser-check', '--disable-translate', '--disable-default-apps', '--disable-popup-blocking', '--disable-zero-browsers-open-for-tests', `--user-data-dir=${userDataDir}`, // possibly confusing "save password" prompt is not possible to // hide because it's only possible by "--enable-automation" ]); const pid = chromeProcess.pid; do { await delayAsync(1000); } while (isRunning(pid) && (await hasNotLoggedIn(pid))); await delayAsync(1000 * 30); //should not been killed immediately chromeProcess.kill(); await delayAsync(1000); await chromePreventCrashDialog(userDataDir); }; const makeLoggedInChromeProfile = async () => { const modulePrefix = 'node-apiless-youtube-upload-'; const tempDir = await createTmpDir(modulePrefix); // Adding a removal exit hook for tempDir is a bad idea, because it cant be // done synchronously for EBUSY reasons (and no async hooks I tried did not // work). Therefore we do a cleanup of previous runs rather than trying to // clean up the current one on exit const { base: tmpBase, dir } = Path.parse(tempDir.path); for (const file of await fs.readdir(dir)) { if (file === tmpBase || !file.startsWith(modulePrefix)) continue; const prevProfilePath = Path.join(dir, file); console.log('Removing temp profile from previous run', prevProfilePath); await fs.rm(prevProfilePath, { recursive: true, force: true, maxRetries: 3 }); } return runUncontrolledChrome(tempDir.path) .then(() => tempDir) .catch((err) => tempDir.remove().then(() => Promise.reject(err))); }; const fetchCookies = async (driver) => { // go to google.com to trigger the saved profile to load faster await driver.get(helpers_1.URL.LOADER); await driver.sleep(4000); await driver.get(helpers_1.URL.GOOGLE); // Open youtube studio to test if the login is valid await driver.get(helpers_1.URL.YOUTUBE_STUDIO); // If cookies are valid, user is now either in Youtube Studio at studio.youtube.com, // or User select page at youtube.com const currentUrl = await driver.getCurrentUrl(); const isLoggedIn = [helpers_1.URL.YOUTUBE, helpers_1.URL.YOUTUBE_STUDIO].some((link) => currentUrl.startsWith(link)); if (!isLoggedIn) { throw new Error('The login session could not be loaded (either user never logged in, ' + "random lag or google account doesn't have a youtube attached)"); } // Select youtube account (one google account might have many brand accounts) await driver.get(helpers_1.URL.SELECT_ACCOUNT_YOUTUBE); // Wait until url matches exactly URL.YOUTUBE. Note that account selection url includes URL.YOUTUBE, which we don't want to match. await driver.wait(selenium_webdriver_1.until.urlIs(helpers_1.URL.YOUTUBE), 60 * 1000); const webDriverCookies = await driver.manage().getCookies(); return new helpers_1.Cookies(webDriverCookies); }; exports.default = async () => { return new Promise((resolve, reject) => { (async () => { let profilePath; let webDriver; profilePath = await makeLoggedInChromeProfile(); webDriver = await (0, helpers_1.makeWebDriver)({ automation: true, userDataDir: profilePath.path }); fetchCookies(webDriver).then(function (cookies) { resolve(cookies); }).catch(function (e) { reject(e); }).finally(async () => { if (webDriver) await webDriver.quit(); if (profilePath) await profilePath.remove(); }); })(); }); };