Skip to content

Instantly share code, notes, and snippets.

@motebaya
Last active February 18, 2026 23:39
Show Gist options
  • Select an option

  • Save motebaya/952bf2a21c4d415d5ee83cf154d44e58 to your computer and use it in GitHub Desktop.

Select an option

Save motebaya/952bf2a21c4d415d5ee83cf154d44e58 to your computer and use it in GitHub Desktop.
raw DOM js element manipulation for tk schedule post
/**
* Core class tiktok uploader with playwright
*
* TIKTOK SCHEDULE RULES:
* minimum: 15 minute future
* maximum: 1 month future
* daily upload: depend the account quality
*/
/**
* page opened -> fresh user -> first time upload to tiktok desktop -> modal appeared.
* first modal is for turned automatic content checking.
*/
const modalFooter = document.querySelector('div[class*="common-modal-footer"]');
if (modalFooter) {
const turnOnBtn = Array.from(modalFooter.querySelectorAll("button")).filter(
(i) => i.textContent.toLowerCase() == "turn on",
)[0];
if (turnOnBtn) {
turnOnBtn.click();
}
}
/**
* 1. upload file via playwright API
* 1.0 target file that to be uploaded should be suplied from user args
*/
file_input = page.locator("//input[@type='file']");
await file_input.set_input_files("sample.mp4");
/**
* 2. check upload finished
* 2.0 playwright must be wait until this element visible
*/
const uploaded = [
...document.querySelectorAll('span[class*="TUXText"]'),
].filter((i) => i.textContent.startsWith("Uploaded"));
if (uploaded.length !== 0) {
uploaded = true;
}
/**
* 3. Set caption/description/hashtags via playwright API
* 3.0 Caption must be suplied from user args
* 3.1 hashtags must be suplied fro user args
*/
locators = page.locator("//div[@contenteditable='true']").first;
page.keyboard.type(char, (delay = 50));
page.keyboard.press("Enter");
/**
* 4. SCHEDULE handlers
* 4.0 click Schedule text
*/
var s = [...document.querySelectorAll("span")].filter(
(i) => i.textContent == "Schedule",
)[0];
s.click();
/**
* if fresh user -> first time upload to tiktok desktop -> modal appeared
* this modal asking user to allow tiktok scheduling videos
*/
const scheduleModal = document.querySelector(
'div[class*="common-modal-footer"]',
);
if (scheduleModal) {
const allowScheduleBtn = Array.from(
scheduleModal.querySelectorAll("button"),
).filter((i) => i.textContent.toLowerCase() == "allow")[0];
if (scheduleModal) {
allowScheduleBtn.click();
}
}
/**
* 4.1 get element hour and date
*/
const [eljam, tanggal] = document.querySelectorAll(
"input[class='TUXTextInputCore-input']",
);
/**
* 4.2 setup the time first by click it
*/
eljam.click();
/**
* 4.3 after click time section, hour and minute will be visible
*/
const [hour, minute] = document.querySelectorAll(
'div[class="tiktok-timepicker-option-list"]',
);
var listjam = [...hour.querySelectorAll("div")];
var listmenit = [...minute.querySelectorAll("div")];
/**
* minute only accept multiply of 5, e.g: 00:05, 00:10, 00:15
* i need better function to handle it
*/
var INPUT_JAM_USER = "03";
var INPUT_MENIT_USER = String(Math.round(parseInt("17") / 5) * 5);
// 4.4 click target hour based suplied from user args
listjam
.filter((i) => i.textContent === INPUT_JAM_USER)[0]
.querySelector("span")
.click();
// 4.5 click click minute based suplied from user args
listmenit
.filter((i) => i.textContent === "30")[0]
.querySelector("span")
.click();
/**
* AFTER setup time finish we will be setup the date
* 4.6 click tanggal element to make it visible
*/
tanggal.click();
/**
* month only accept text , but input user always number
* need better function to handle date parsing
*/
var TANGGAL = 23;
var BULAN = 2;
var TAHUN = 2026;
const month = {
"01": "January",
"02": "February",
"03": "March",
"04": "April",
"05": "May",
"06": "June",
"07": "July",
"08": "August",
"09": "September",
10: "October",
11: "November",
12: "December",
};
var monthstr = month[String(BULAN).padStart(2, 0)];
// 4.7 calendar
var calender = document.querySelector(".calendar-wrapper");
// if month name not match with month str , will be switch to next month until matched
while (1) {
// 4.8 get month title from calendar
var monthtitle = calender.querySelector(".month-title").textContent;
if (monthtitle.trim() !== monthstr) {
// 4.9 click arrow right to switch month
var [sleft, sright] = calender.querySelectorAll(".arrow");
// click next month
sright.click();
// delay 1 was enough
await new Promise(resolve, setTimeout(resolve, 1000));
} else {
// stop if date match
break;
}
}
// 5.0 clik day based suplied from user day args
[...calender.querySelectorAll('span[class*="day"]')]
.filter((i) => i.textContent === String(TANGGAL))[0]
.click();
// 5 Change visibility
// 5.1 click video visibility drowpdown to make it visibile
document
.querySelector('div[class*="view-auth-container"]')
.querySelector("button")
.click();
/**
* need better function to handle visibility args
*/
var visibility = {
private: "Only you",
public: "Everyone",
friends: "Friends",
};
var VISIBILITY_USER = "private"; // can be friends, public
// 5.2 get drowpdown option visibility
var options_visibility = document.querySelectorAll(
'div[class*="select-option"]',
);
// 5.3 click based suply user
[...options_visibility]
.filter((i) => i.textContent.startsWith(visibility[VISIBILITY_USER]))[0]
.click();
// 5.4 click post button
document.querySelector('button[data-e2e="post_video_button"]').click();
// 5.5 after click post/Schedule button there is a toast message at top
// check to make sure that toast message is not not upload limit
let toast = document.querySelector('div[class*="Toast-content"]');
if (toast) {
toast = toast.textContent.trim();
if (
!/video\s+published/is.test(toast) &&
!/video\s+has\s+been\s+uploaded/is.test(toast)
) {
// upload limit
}
}
// done
//copyright check
// must be : 'No issues found.' for music
document.querySelector('div[class*="status-success"]').querySelector("span")
.textContent;
// content check
// status-ready -> status-checking -> status-success/warn/error/limit
// must wait until status-success/warn/error/limit
let stat = {
statuses: {
"status-ready": {
state: "ready",
severity: "neutral",
message: "We’ll check your content for For You Feed eligibility.",
action: null,
retry: false,
loading: false,
},
"status-checking": {
state: "checking",
severity: "info",
message:
"Checking in progress. This usually takes about 10 minutes. Longer videos may take more time.",
action: null,
retry: false,
loading: true,
},
"status-success": {
state: "success",
severity: "success",
message:
"No issues detected. However, your video may still be removed later if it violates Community Guidelines.",
action: null,
retry: false,
loading: false,
},
"status-warn": {
state: "warning",
severity: "warning",
message:
"Content may be restricted. You can still post, but modifying it to follow guidelines may improve visibility.",
action: {
label: "View details",
type: "link",
},
retry: false,
loading: false,
},
"status-error": {
state: "error",
severity: "error",
message: "Something went wrong. Please try again later.",
action: {
label: "Retry",
type: "button",
},
retry: true,
loading: false,
},
// originally "status-ready"
"status-limit": {
state: "limitReached",
severity: "warning",
message:
"You’ve reached your check limit for today. Please try again tomorrow.",
action: null,
retry: false,
loading: false,
},
"status-not-eligible": {
state: "notEligible",
severity: "warning",
message:
"This feature isn’t available for government, politician, or political party accounts.",
action: null,
retry: false,
loading: false,
},
},
};
// state ready and state limit have same status key, but they are have different message
let statusActiveKey;
let keystatus = document.querySelector(
'div[class*="status-result"][data-show="true"]',
);
if (/try\s+again\s+tomorrow/is.test(keystatus.textContent)) {
statusActiveKey = "status-limit";
} else {
statusActiveKey = Array.from(keystatus.classList).filter(
(i) => i.startsWith("status") && !i.endsWith("result"),
)[0];
}
// must not in checking statuse
var message = stat[statusActiveKey];
// if statuskey == status-warn, sometime warning modal will be appeared
// wait 2-3s and then close it!
if (statusActiveKey === "status-warn") {
const modal_warning = document.querySelector(
'div[class*="common-modal-close-icon"]',
);
if (modal_warning) {
modal_warning.click();
return true;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment