Skip to content

Publish all your draft videos without clicking by using javascript

Posted on:July 1, 2024 at 04:06 AM

https://github.com/Niedzwiedzw/youtube-publish-drafts

This script works better on Firefox, in my experience.

(() => {
  // -----------------------------------------------------------------
  // CONFIG (you're safe to edit this)
  // -----------------------------------------------------------------
  // ~ GLOBAL CONFIG
  // -----------------------------------------------------------------
  const MODE = "publish_drafts"; // 'publish_drafts' / 'sort_playlist';
  const DEBUG_MODE = true; // true / false, enable for more context
  // -----------------------------------------------------------------
  // ~ PUBLISH CONFIG
  // -----------------------------------------------------------------
  const MADE_FOR_KIDS = false; // true / false;
  const VISIBILITY = "Public"; // 'Public' / 'Private' / 'Unlisted'
  // -----------------------------------------------------------------
  // ~ SORT PLAYLIST CONFIG
  // -----------------------------------------------------------------
  const SORTING_KEY = (one, other) => {
    return one.name.localeCompare(other.name, undefined, {
      numeric: true,
      sensitivity: "base",
    });
  };
  // END OF CONFIG (not safe to edit stuff below)
  // -----------------------------------------------------------------

  // Art by Joan G. Stark
  // .'"'.        ___,,,___        .'``.
  // : (\  `."'"```         ```"'"-'  /) ;
  //  :  \                         `./  .'
  //   `.                            :.'
  //     /        _         _        \
  //    |         0}       {0         |
  //    |         /         \         |
  //    |        /           \        |
  //    |       /             \       |
  //     \     |      .-.      |     /
  //      `.   | . . /   \ . . |   .'
  //        `-._\.'.(     ).'./_.-'
  //            `\'  `._.'  '/'
  //              `. --'-- .'
  //                `-...-'

  // ----------------------------------
  // COMMON  STUFF
  // ---------------------------------
  const TIMEOUT_STEP_MS = 20;
  const DEFAULT_ELEMENT_TIMEOUT_MS = 10000;
  function debugLog(...args) {
    if (!DEBUG_MODE) {
      return;
    }
    console.debug(...args);
  }
  const sleep = ms => new Promise((resolve, _) => setTimeout(resolve, ms));

  async function waitForElement(selector, baseEl, timeoutMs) {
    if (timeoutMs === undefined) {
      timeoutMs = DEFAULT_ELEMENT_TIMEOUT_MS;
    }
    if (baseEl === undefined) {
      baseEl = document;
    }
    let timeout = timeoutMs;
    while (timeout > 0) {
      let element = baseEl.querySelector(selector);
      if (element !== null) {
        return element;
      }
      await sleep(TIMEOUT_STEP_MS);
      timeout -= TIMEOUT_STEP_MS;
    }
    debugLog(`could not find ${selector} inside`, baseEl);
    return null;
  }

  function click(element) {
    const event = document.createEvent("MouseEvents");
    event.initMouseEvent(
      "mousedown",
      true,
      false,
      window,
      0,
      0,
      0,
      0,
      0,
      false,
      false,
      false,
      false,
      0,
      null
    );
    element.dispatchEvent(event);
    element.click();
    debugLog(element, "clicked");
  }

  // ----------------------------------
  // PUBLISH STUFF
  // ----------------------------------
  const VISIBILITY_PUBLISH_ORDER = {
    Private: 0,
    Unlisted: 1,
    Public: 2,
  };

  // SELECTORS
  // ---------
  const VIDEO_ROW_SELECTOR = "ytcp-video-row";
  const DRAFT_MODAL_SELECTOR = ".style-scope.ytcp-uploads-dialog";
  const DRAFT_BUTTON_SELECTOR = ".edit-draft-button";
  const MADE_FOR_KIDS_SELECTOR = "#made-for-kids-group";
  const RADIO_BUTTON_SELECTOR = "tp-yt-paper-radio-button";
  const VISIBILITY_STEPPER_SELECTOR = "#step-badge-3";
  const VISIBILITY_PAPER_BUTTONS_SELECTOR = "tp-yt-paper-radio-group";
  const SAVE_BUTTON_SELECTOR = "#done-button";
  const SUCCESS_ELEMENT_SELECTOR = "ytcp-video-thumbnail-with-info";
  const DIALOG_SELECTOR =
    "ytcp-dialog.ytcp-video-share-dialog > tp-yt-paper-dialog:nth-child(1)";
  const DIALOG_CLOSE_BUTTON_SELECTOR = "tp-yt-iron-icon";

  class SuccessDialog {
    constructor(raw) {
      this.raw = raw;
    }

    async closeDialogButton() {
      return await waitForElement(DIALOG_CLOSE_BUTTON_SELECTOR, this.raw);
    }

    async close() {
      click(await this.closeDialogButton());
      await sleep(50);
      debugLog("closed");
    }
  }

  class VisibilityModal {
    constructor(raw) {
      this.raw = raw;
    }

    async radioButtonGroup() {
      return await waitForElement(VISIBILITY_PAPER_BUTTONS_SELECTOR, this.raw);
    }

    async visibilityRadioButton() {
      const group = await this.radioButtonGroup();
      const value = VISIBILITY_PUBLISH_ORDER[VISIBILITY];
      return [...group.querySelectorAll(RADIO_BUTTON_SELECTOR)][value];
    }

    async setVisibility() {
      click(await this.visibilityRadioButton());
      debugLog(`visibility set to ${VISIBILITY}`);
      await sleep(50);
    }

    async saveButton() {
      return await waitForElement(SAVE_BUTTON_SELECTOR, this.raw);
    }
    async isSaved() {
      await waitForElement(SUCCESS_ELEMENT_SELECTOR, document);
    }
    async dialog() {
      return await waitForElement(DIALOG_SELECTOR);
    }
    async save() {
      click(await this.saveButton());
      await this.isSaved();
      debugLog("saved");
      const dialogElement = await this.dialog();
      const success = new SuccessDialog(dialogElement);
      return success;
    }
  }

  class DraftModal {
    constructor(raw) {
      this.raw = raw;
    }

    async madeForKidsToggle() {
      return await waitForElement(MADE_FOR_KIDS_SELECTOR, this.raw);
    }

    async madeForKidsPaperButton() {
      const nthChild = MADE_FOR_KIDS ? 1 : 2;
      return await waitForElement(
        `${RADIO_BUTTON_SELECTOR}:nth-child(${nthChild})`,
        this.raw
      );
    }

    async selectMadeForKids() {
      click(await this.madeForKidsPaperButton());
      await sleep(50);
      debugLog(`"Made for kids" set as ${MADE_FOR_KIDS}`);
    }

    async visibilityStepper() {
      return await waitForElement(VISIBILITY_STEPPER_SELECTOR, this.raw);
    }

    async goToVisibility() {
      debugLog("going to Visibility");
      await sleep(50);
      click(await this.visibilityStepper());
      const visibility = new VisibilityModal(this.raw);
      await sleep(50);
      await waitForElement(VISIBILITY_PAPER_BUTTONS_SELECTOR, visibility.raw);
      return visibility;
    }
  }

  class VideoRow {
    constructor(raw) {
      this.raw = raw;
    }

    get editDraftButton() {
      return waitForElement(DRAFT_BUTTON_SELECTOR, this.raw, 20);
    }

    async openDraft() {
      debugLog("focusing draft button");
      click(await this.editDraftButton);
      return new DraftModal(await waitForElement(DRAFT_MODAL_SELECTOR));
    }
  }

  function allVideos() {
    return [...document.querySelectorAll(VIDEO_ROW_SELECTOR)].map(
      el => new VideoRow(el)
    );
  }

  async function editableVideos() {
    let editable = [];
    for (let video of allVideos()) {
      if ((await video.editDraftButton) !== null) {
        editable = [...editable, video];
      }
    }
    return editable;
  }

  async function publishDrafts() {
    const videos = await editableVideos();
    debugLog(`found ${videos.length} videos`);
    debugLog("starting in 1000ms");
    await sleep(1000);
    for (let video of videos) {
      const draft = await video.openDraft();
      debugLog({
        draft,
      });
      await draft.selectMadeForKids();
      const visibility = await draft.goToVisibility();
      await visibility.setVisibility();
      const dialog = await visibility.save();
      await dialog.close();
      await sleep(100);
    }
  }

  // ----------------------------------
  // SORTING STUFF
  // ----------------------------------
  const SORTING_MENU_BUTTON_SELECTOR = "button";
  const SORTING_ITEM_MENU_SELECTOR = "tp-yt-paper-listbox#items";
  const SORTING_ITEM_MENU_ITEM_SELECTOR = "ytd-menu-service-item-renderer";
  const MOVE_TO_TOP_INDEX = 4;
  const MOVE_TO_BOTTOM_INDEX = 5;

  class SortingDialog {
    constructor(raw) {
      this.raw = raw;
    }

    async anyMenuItem() {
      const item = await waitForElement(
        SORTING_ITEM_MENU_ITEM_SELECTOR,
        this.raw
      );
      if (item === null) {
        throw new Error("could not locate any menu item");
      }
      return item;
    }

    menuItems() {
      return [...this.raw.querySelectorAll(SORTING_ITEM_MENU_ITEM_SELECTOR)];
    }

    async moveToTop() {
      click(this.menuItems()[MOVE_TO_TOP_INDEX]);
    }

    async moveToBottom() {
      click(this.menuItems()[MOVE_TO_BOTTOM_INDEX]);
    }
  }
  class PlaylistVideo {
    constructor(raw) {
      this.raw = raw;
    }
    get name() {
      return this.raw.querySelector("#video-title").textContent;
    }
    async dialog() {
      return this.raw.querySelector(SORTING_MENU_BUTTON_SELECTOR);
    }

    async openDialog() {
      click(await this.dialog());
      const dialog = new SortingDialog(
        await waitForElement(SORTING_ITEM_MENU_SELECTOR)
      );
      await dialog.anyMenuItem();
      return dialog;
    }
  }
  async function playlistVideos() {
    return [...document.querySelectorAll("ytd-playlist-video-renderer")].map(
      el => new PlaylistVideo(el)
    );
  }
  async function sortPlaylist() {
    debugLog("sorting playlist");
    const videos = await playlistVideos();
    debugLog(`found ${videos.length} videos`);
    videos.sort(SORTING_KEY);
    const videoNames = videos.map(v => v.name);

    let index = 1;
    for (let name of videoNames) {
      debugLog({ index, name });
      const video = videos.find(v => v.name === name);
      const dialog = await video.openDialog();
      await dialog.moveToBottom();
      await sleep(1000);
      index += 1;
    }
  }

  // ----------------------------------
  // ENTRY POINT
  // ----------------------------------
  ({
    publish_drafts: publishDrafts,
    sort_playlist: sortPlaylist,
  })[MODE]();
})();