Last active
February 18, 2026 23:39
-
-
Save motebaya/952bf2a21c4d415d5ee83cf154d44e58 to your computer and use it in GitHub Desktop.
raw DOM js element manipulation for tk schedule post
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * 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