출처: https://www.youtube.com/watch?v=VvOOIntf6jE Thanks, Tech Upskill - Khalil Sheikh Salem
allow pasting 을 눌러 스크립트를 붙여넣기 할 수 있도록 수정한다.
- 콘텐츠 항목으로 이동한다.
- 개발자 도구(Dev tools) 를 연다.
중단을 원하는 경우, 새로고침(윈도우즈 경우 F5)하면 된다.
(() => {
// -----------------------------------------------------------------
// CONFIG (you're safe to edit this)
// -----------------------------------------------------------------
const MODE = 'publish_drafts'; // 'publish_drafts' / 'sort_playlist';
const DEBUG_MODE = true;
// -----------------------------------------------------------------
// ~ PUBLISH CONFIG
// -----------------------------------------------------------------
const MADE_FOR_KIDS = false; // true / false;
const VISIBILITY = 'Public'; // 'Public' / 'Private' / 'Unlisted'
// -----------------------------------------------------------------
// ~ SORT PLAYLIST CONFIG
// -----------------------------------------------------------------
const SORTING_KEY = (one, other) => {
const numberRegex = /\d+/;
const number = (name) => name.match(numberRegex)[0];
if (number(one.name) === undefined || number(other.name) === undefined) {
return one.name.localeCompare(other.name);
}
return number(one.name) - number(other.name);
};
// ----------------------------------
// 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) {
if (!element) {
debugLog('Element is null, skipping click.');
return;
}
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,
};
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 = 'button[aria-label="닫기"], button[aria-label="Close"], #close-button, #close-icon-button, tp-yt-iron-icon';
class SuccessDialog {
constructor(raw) {
this.raw = raw;
}
async closeDialogButton() {
let btn = await waitForElement(DIALOG_CLOSE_BUTTON_SELECTOR, this.raw, 5000);
if (!btn) {
btn = document.querySelector(DIALOG_CLOSE_BUTTON_SELECTOR);
}
return btn;
}
async close() {
const btn = await this.closeDialogButton();
if (btn) {
click(btn);
await sleep(500);
debugLog('closed via button');
} else {
debugLog('Close button not found. Attempting to close with Escape key...');
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
await sleep(500);
}
}
}
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, document, 3000);
}
async save() {
click(await this.saveButton());
await this.isSaved();
debugLog('saved');
const dialogElement = await this.dialog();
return new SuccessDialog(dialogElement);
}
}
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.push(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();
// 다음 작업 전 UI 안정화를 위한 대기 시간 유지
await sleep(1500);
}
}
// ----------------------------------
// SORTING STUFF
// ----------------------------------
const SORTING_MENU_BUTTON_SELECTOR = 'button';
const SORTING_ITEM_MENU_SELECTOR = '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]();
})();