├── .gitignore ├── youtube-publisher-demo.gif ├── README.md ├── LICENSE └── youtube-publish-drafts.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .log -------------------------------------------------------------------------------- /youtube-publisher-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Niedzwiedzw/youtube-publish-drafts/HEAD/youtube-publisher-demo.gif -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # youtube-publish-drafts 2 | 3 | ## help 4 | to get help join our discord server https://discord.gg/xj6JxW8k 5 | 6 | ## support development of this project 7 | ``` 8 | BTC: bc1qksrtrwkhq043h56rsh9d4zdnmk0d43tm4m6xux 9 | ``` 10 | 11 | ## about 12 | Publish all your draft videos without clicking by using javascript 13 | ![quick demo](youtube-publisher-demo.gif) 14 | 15 | ## how it would be used 16 | if you were to do that, (which I'm pretty sure is against YouTube's terms of use, so don't do that), you'd: 17 | 1. go to YouTube Studio's "Content" page 18 | 2. press f12 19 | 3. paste the entire content of youtube-publish-drafts.js file 20 | 4. wait 21 | 22 | It should work as of 26.12.2020. 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Wojciech Niedźwiedź 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /youtube-publish-drafts.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | // ----------------------------------------------------------------- 3 | // CONFIG (you're safe to edit this) 4 | // ----------------------------------------------------------------- 5 | // ~ GLOBAL CONFIG 6 | // ----------------------------------------------------------------- 7 | const MODE = 'publish_drafts'; // 'publish_drafts' / 'sort_playlist'; 8 | const DEBUG_MODE = true; // true / false, enable for more context 9 | // ----------------------------------------------------------------- 10 | // ~ PUBLISH CONFIG 11 | // ----------------------------------------------------------------- 12 | const MADE_FOR_KIDS = false; // true / false; 13 | const VISIBILITY = 'Public'; // 'Public' / 'Private' / 'Unlisted' 14 | // ----------------------------------------------------------------- 15 | // ~ SORT PLAYLIST CONFIG 16 | // ----------------------------------------------------------------- 17 | const SORTING_KEY = (one, other) => { 18 | return one.name.localeCompare(other.name, undefined, {numeric: true, sensitivity: 'base'}); 19 | }; 20 | // END OF CONFIG (not safe to edit stuff below) 21 | // ----------------------------------------------------------------- 22 | 23 | // Art by Joan G. Stark 24 | // .'"'. ___,,,___ .'``. 25 | // : (\ `."'"``` ```"'"-' /) ; 26 | // : \ `./ .' 27 | // `. :.' 28 | // / _ _ \ 29 | // | 0} {0 | 30 | // | / \ | 31 | // | / \ | 32 | // | / \ | 33 | // \ | .-. | / 34 | // `. | . . / \ . . | .' 35 | // `-._\.'.( ).'./_.-' 36 | // `\' `._.' '/' 37 | // `. --'-- .' 38 | // `-...-' 39 | 40 | 41 | 42 | // ---------------------------------- 43 | // COMMON STUFF 44 | // --------------------------------- 45 | const TIMEOUT_STEP_MS = 20; 46 | const DEFAULT_ELEMENT_TIMEOUT_MS = 10000; 47 | function debugLog(...args) { 48 | if (!DEBUG_MODE) { 49 | return; 50 | } 51 | console.debug(...args); 52 | } 53 | const sleep = (ms) => new Promise((resolve, _) => setTimeout(resolve, ms)); 54 | 55 | async function waitForElement(selector, baseEl, timeoutMs) { 56 | if (timeoutMs === undefined) { 57 | timeoutMs = DEFAULT_ELEMENT_TIMEOUT_MS; 58 | } 59 | if (baseEl === undefined) { 60 | baseEl = document; 61 | } 62 | let timeout = timeoutMs; 63 | while (timeout > 0) { 64 | let element = baseEl.querySelector(selector); 65 | if (element !== null) { 66 | return element; 67 | } 68 | await sleep(TIMEOUT_STEP_MS); 69 | timeout -= TIMEOUT_STEP_MS; 70 | } 71 | debugLog(`could not find ${selector} inside`, baseEl); 72 | return null; 73 | } 74 | 75 | function click(element) { 76 | const event = document.createEvent('MouseEvents'); 77 | event.initMouseEvent('mousedown', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); 78 | element.dispatchEvent(event); 79 | element.click(); 80 | debugLog(element, 'clicked'); 81 | } 82 | 83 | // ---------------------------------- 84 | // PUBLISH STUFF 85 | // ---------------------------------- 86 | const VISIBILITY_PUBLISH_ORDER = { 87 | 'Private': 0, 88 | 'Unlisted': 1, 89 | 'Public': 2, 90 | }; 91 | 92 | // SELECTORS 93 | // --------- 94 | const VIDEO_ROW_SELECTOR = 'ytcp-video-row'; 95 | const DRAFT_MODAL_SELECTOR = '.style-scope.ytcp-uploads-dialog'; 96 | const DRAFT_BUTTON_SELECTOR = '.edit-draft-button'; 97 | const MADE_FOR_KIDS_SELECTOR = '#made-for-kids-group'; 98 | const RADIO_BUTTON_SELECTOR = 'tp-yt-paper-radio-button'; 99 | const VISIBILITY_STEPPER_SELECTOR = '#step-badge-3'; 100 | const VISIBILITY_PAPER_BUTTONS_SELECTOR = 'tp-yt-paper-radio-group'; 101 | const SAVE_BUTTON_SELECTOR = '#done-button'; 102 | const SUCCESS_ELEMENT_SELECTOR = 'ytcp-video-thumbnail-with-info'; 103 | const DIALOG_SELECTOR = 'ytcp-dialog.ytcp-video-share-dialog > tp-yt-paper-dialog:nth-child(1)'; 104 | const DIALOG_CLOSE_BUTTON_SELECTOR = 'tp-yt-iron-icon'; 105 | 106 | class SuccessDialog { 107 | constructor(raw) { 108 | this.raw = raw; 109 | } 110 | 111 | async closeDialogButton() { 112 | return await waitForElement(DIALOG_CLOSE_BUTTON_SELECTOR, this.raw); 113 | } 114 | 115 | async close() { 116 | click(await this.closeDialogButton()); 117 | await sleep(50); 118 | debugLog('closed'); 119 | } 120 | } 121 | 122 | class VisibilityModal { 123 | constructor(raw) { 124 | this.raw = raw; 125 | } 126 | 127 | async radioButtonGroup() { 128 | return await waitForElement(VISIBILITY_PAPER_BUTTONS_SELECTOR, this.raw); 129 | } 130 | 131 | async visibilityRadioButton() { 132 | const group = await this.radioButtonGroup(); 133 | const value = VISIBILITY_PUBLISH_ORDER[VISIBILITY]; 134 | return [...group.querySelectorAll(RADIO_BUTTON_SELECTOR)][value]; 135 | } 136 | 137 | async setVisibility() { 138 | click(await this.visibilityRadioButton()); 139 | debugLog(`visibility set to ${VISIBILITY}`); 140 | await sleep(50); 141 | } 142 | 143 | async saveButton() { 144 | return await waitForElement(SAVE_BUTTON_SELECTOR, this.raw); 145 | } 146 | async isSaved() { 147 | await waitForElement(SUCCESS_ELEMENT_SELECTOR, document); 148 | } 149 | async dialog() { 150 | return await waitForElement(DIALOG_SELECTOR); 151 | } 152 | async save() { 153 | click(await this.saveButton()); 154 | await this.isSaved(); 155 | debugLog('saved'); 156 | const dialogElement = await this.dialog(); 157 | const success = new SuccessDialog(dialogElement); 158 | return success; 159 | } 160 | } 161 | 162 | class DraftModal { 163 | constructor(raw) { 164 | this.raw = raw; 165 | } 166 | 167 | async madeForKidsToggle() { 168 | return await waitForElement(MADE_FOR_KIDS_SELECTOR, this.raw); 169 | } 170 | 171 | async madeForKidsPaperButton() { 172 | const nthChild = MADE_FOR_KIDS ? 1 : 2; 173 | return await waitForElement(`${RADIO_BUTTON_SELECTOR}:nth-child(${nthChild})`, this.raw); 174 | } 175 | 176 | async selectMadeForKids() { 177 | click(await this.madeForKidsPaperButton()); 178 | await sleep(50); 179 | debugLog(`"Made for kids" set as ${MADE_FOR_KIDS}`); 180 | } 181 | 182 | async visibilityStepper() { 183 | return await waitForElement(VISIBILITY_STEPPER_SELECTOR, this.raw); 184 | } 185 | 186 | async goToVisibility() { 187 | debugLog('going to Visibility'); 188 | await sleep(50); 189 | click(await this.visibilityStepper()); 190 | const visibility = new VisibilityModal(this.raw); 191 | await sleep(50); 192 | await waitForElement(VISIBILITY_PAPER_BUTTONS_SELECTOR, visibility.raw); 193 | return visibility; 194 | } 195 | } 196 | 197 | class VideoRow { 198 | constructor(raw) { 199 | this.raw = raw; 200 | } 201 | 202 | get editDraftButton() { 203 | return waitForElement(DRAFT_BUTTON_SELECTOR, this.raw, 20); 204 | } 205 | 206 | async openDraft() { 207 | debugLog('focusing draft button'); 208 | click(await this.editDraftButton); 209 | return new DraftModal(await waitForElement(DRAFT_MODAL_SELECTOR)); 210 | } 211 | } 212 | 213 | 214 | function allVideos() { 215 | return [...document.querySelectorAll(VIDEO_ROW_SELECTOR)].map((el) => new VideoRow(el)); 216 | } 217 | 218 | async function editableVideos() { 219 | let editable = []; 220 | for (let video of allVideos()) { 221 | if ((await video.editDraftButton) !== null) { 222 | editable = [...editable, video]; 223 | } 224 | } 225 | return editable; 226 | } 227 | 228 | async function publishDrafts() { 229 | const videos = await editableVideos(); 230 | debugLog(`found ${videos.length} videos`); 231 | debugLog('starting in 1000ms'); 232 | await sleep(1000); 233 | for (let video of videos) { 234 | const draft = await video.openDraft(); 235 | debugLog({ 236 | draft 237 | }); 238 | await draft.selectMadeForKids(); 239 | const visibility = await draft.goToVisibility(); 240 | await visibility.setVisibility(); 241 | const dialog = await visibility.save(); 242 | await dialog.close(); 243 | await sleep(100); 244 | } 245 | } 246 | 247 | // ---------------------------------- 248 | // SORTING STUFF 249 | // ---------------------------------- 250 | const SORTING_MENU_BUTTON_SELECTOR = 'button'; 251 | const SORTING_ITEM_MENU_SELECTOR = 'tp-yt-paper-listbox#items'; 252 | const SORTING_ITEM_MENU_ITEM_SELECTOR = 'ytd-menu-service-item-renderer'; 253 | const MOVE_TO_TOP_INDEX = 4; 254 | const MOVE_TO_BOTTOM_INDEX = 5; 255 | 256 | class SortingDialog { 257 | constructor(raw) { 258 | this.raw = raw; 259 | } 260 | 261 | async anyMenuItem() { 262 | const item = await waitForElement(SORTING_ITEM_MENU_ITEM_SELECTOR, this.raw); 263 | if (item === null) { 264 | throw new Error("could not locate any menu item"); 265 | } 266 | return item; 267 | } 268 | 269 | menuItems() { 270 | return [...this.raw.querySelectorAll(SORTING_ITEM_MENU_ITEM_SELECTOR)]; 271 | } 272 | 273 | async moveToTop() { 274 | click(this.menuItems()[MOVE_TO_TOP_INDEX]); 275 | } 276 | 277 | async moveToBottom() { 278 | click(this.menuItems()[MOVE_TO_BOTTOM_INDEX]); 279 | } 280 | } 281 | class PlaylistVideo { 282 | constructor(raw) { 283 | this.raw = raw; 284 | } 285 | get name() { 286 | return this.raw.querySelector('#video-title').textContent; 287 | } 288 | async dialog() { 289 | return this.raw.querySelector(SORTING_MENU_BUTTON_SELECTOR); 290 | } 291 | 292 | async openDialog() { 293 | click(await this.dialog()); 294 | const dialog = new SortingDialog(await waitForElement(SORTING_ITEM_MENU_SELECTOR)); 295 | await dialog.anyMenuItem(); 296 | return dialog; 297 | } 298 | 299 | } 300 | async function playlistVideos() { 301 | return [...document.querySelectorAll('ytd-playlist-video-renderer')] 302 | .map((el) => new PlaylistVideo(el)); 303 | } 304 | async function sortPlaylist() { 305 | debugLog('sorting playlist'); 306 | const videos = await playlistVideos(); 307 | debugLog(`found ${videos.length} videos`); 308 | videos.sort(SORTING_KEY); 309 | const videoNames = videos.map((v) => v.name); 310 | 311 | let index = 1; 312 | for (let name of videoNames) { 313 | debugLog({index, name}); 314 | const video = videos.find((v) => v.name === name); 315 | const dialog = await video.openDialog(); 316 | await dialog.moveToBottom(); 317 | await sleep(1000); 318 | index += 1; 319 | } 320 | 321 | } 322 | 323 | 324 | // ---------------------------------- 325 | // ENTRY POINT 326 | // ---------------------------------- 327 | ({ 328 | 'publish_drafts': publishDrafts, 329 | 'sort_playlist': sortPlaylist, 330 | })[MODE](); 331 | 332 | 333 | })(); 334 | 335 | --------------------------------------------------------------------------------