Skip to content

Commit

Permalink
Merge pull request Schroedinger-Hat#100 from Schrodinger-Hat/bugfix/#…
Browse files Browse the repository at this point in the history
…99_fix_script_to_work_on_the_updated_website_ui

Bugfix/Schroedinger-Hat#99 fix script to work on the updated website UI
  • Loading branch information
matevskial authored Oct 22, 2023
2 parents ab8c56f + 08faffb commit 4208192
Show file tree
Hide file tree
Showing 4 changed files with 309 additions and 163 deletions.
317 changes: 177 additions & 140 deletions src/anchorfm-pupeteer/index.js
Original file line number Diff line number Diff line change
@@ -1,152 +1,186 @@
const puppeteer = require('puppeteer');
const env = require('../environment-variables');
const { compareDates } = require('../dateutils');
const { isEmpty } = require('../stringutils');

function addUrlToDescription(youtubeVideoInfo) {
return env.URL_IN_DESCRIPTION
? `${youtubeVideoInfo.description}\n${youtubeVideoInfo.url}`
: youtubeVideoInfo.description;
}

async function setPublishDate(page, navigationPromise, date) {
async function setPublishDate(page, date) {
console.log('-- Setting publish date');
await clickXpath(page, '//span[contains(text(),"Publish date:")]/following-sibling::button');
await navigationPromise;

await resetDatePickerToSelectYears(page, navigationPromise);
await selectYearInDatePicker(page, navigationPromise, date.year);
await selectMonthInDatePicker(page, navigationPromise, date.month);
await selectDayInDatePicker(page, navigationPromise, date.day);

await clickXpath(page, '//span[contains(text(),"Confirm")]/parent::button');
await navigationPromise;
}
await clickSelector(page, 'input[type="radio"][id="publish-date-schedule"]');
await page.waitForSelector('#date-input', { visible: true });
await clickSelector(page, '#date-input');

await selectCorrectYearAndMonthInDatePicker();
await selectCorrectDayInDatePicker();

async function selectCorrectYearAndMonthInDatePicker() {
const dateForComparison = `${date.monthAsFullWord} ${date.year}`;
const currentDateCaptionElementSelector =
'div[class*="CalendarMonth"][data-visible="true"] div[class*="CalendarMonth_caption"] > strong';
let currentDate = await getTextContentFromSelector(page, currentDateCaptionElementSelector);
const navigationButtonSelector =
compareDates(dateForComparison, currentDate) === -1
? 'div[class*="DayPickerNavigation_leftButton__horizontalDefault"]'
: 'div[class*="DayPickerNavigation_rightButton__horizontalDefault"]';

while (currentDate !== dateForComparison) {
await clickSelector(page, navigationButtonSelector);
currentDate = await getTextContentFromSelector(page, currentDateCaptionElementSelector);
}
}

async function resetDatePickerToSelectYears(page, navigationPromise) {
for (let i = 0; i < 2; i += 1) {
await clickSelector(page, 'th[class="rdtSwitch"]');
await navigationPromise;
async function selectCorrectDayInDatePicker() {
const dayWithoutLeadingZero = parseInt(date.day, 10);
const dayXpath = `//div[contains(@class, "CalendarMonth") and @data-visible="true"]//td[contains(text(), "${dayWithoutLeadingZero}")]`;
await clickXpath(page, dayXpath);
}
}

async function selectYearInDatePicker(page, navigationPromise, year) {
const rdtPrev = await page.$('th[class="rdtPrev"]');
let currentLowestYear = await page.$eval('tbody > tr:first-child > td:first-child', (e) =>
e.getAttribute('data-value')
);
while (parseInt(currentLowestYear, 10) > parseInt(year, 10)) {
await rdtPrev.click();
await navigationPromise;

currentLowestYear = await page.$eval('tbody > tr:first-child > td:first-child', (e) =>
e.getAttribute('data-value')
);
}
async function postEpisode(youtubeVideoInfo) {
let browser;
let page;

const rdtNext = await page.$('th[class="rdtNext"]');
let currentHighestYear = await page.$eval('tbody > tr:last-child > td:last-child', (e) =>
e.getAttribute('data-value')
);
while (parseInt(currentHighestYear, 10) < parseInt(year, 10)) {
await rdtNext.click();
await navigationPromise;
try {
console.log('Launching puppeteer');
browser = await puppeteer.launch({ args: ['--no-sandbox'], headless: env.PUPETEER_HEADLESS });

currentHighestYear = await page.$eval('tbody > tr:last-child > td:last-child', (e) => e.getAttribute('data-value'));
}
page = await openNewPage('https://podcasters.spotify.com/pod/dashboard/episode/wizard');

await clickSelector(page, `tbody > tr > td[data-value="${year}"]`);
await navigationPromise;
}
console.log('Setting language to English');
await setLanguageToEnglish();

async function selectMonthInDatePicker(page, navigationPromise, month) {
await clickXpath(page, `//tbody/tr/td[contains(text(),"${month}")]`);
await navigationPromise;
}
console.log('Trying to log in');
await login();

async function selectDayInDatePicker(page, navigationPromise, day) {
const dayWithRemovedZeroPad = parseInt(day, 10);
await clickSelector(
page,
`tbody > tr > td[data-value="${dayWithRemovedZeroPad}"][class*="rdtDay"]:not([class*="rdtOld"]):not([class*="rtdNew"])`
);
await navigationPromise;
}
console.log('Opening new episode wizard');
await waitForNewEpisodeWizard();

async function setLanguageToEnglish(page) {
await clickSelector(page, 'button[aria-label="Change language"]');
await clickSelector(page, 'div[aria-label="Language selection modal"] a[data-testid="language-option-en"]');
}
console.log('Uploading audio file');
await uploadEpisode();

async function postEpisode(youtubeVideoInfo) {
let browser;
try {
console.log('Launching puppeteer');
browser = await puppeteer.launch({ args: ['--no-sandbox'], headless: env.PUPETEER_HEADLESS });
const page = await browser.newPage();
console.log('Filling required podcast details');
await fillRequiredDetails();

const navigationPromise = page.waitForNavigation();
console.log('Filling optional podcast details');
await fillOptionalDetails();

await page.goto('https://podcasters.spotify.com/pod/dashboard/episode/new');
console.log('Skipping Interact step');
await skipInteractStep();

await page.setViewport({ width: 1600, height: 789 });
console.log('Save draft or publish');
await saveDraftOrScheduleOrPublish();

await navigationPromise;
/*
This is a workaround solution of the problem where the podcast
is sometimes saved as draft with title "Untitled" and no other metadata.
We navigate to the spotify/anchorfm dashboard immediately after podcast is
published/scheduled.
*/
await goToDashboard();

console.log('Setting language to English');
await setLanguageToEnglish(page);
console.log('Yay');
} catch (err) {
if (page !== undefined) {
console.log('Screenshot base64:');
const screenshotBase64 = await page.screenshot({ encoding: 'base64' });
console.log(`data:image/png;base64,${screenshotBase64}`);
}
throw new Error(`Unable to post episode to anchorfm: ${err}`);
} finally {
if (browser !== undefined) {
await browser.close();
}
}

console.log('Accessing Log in with email');
async function openNewPage(url) {
const newPage = await browser.newPage();
await newPage.goto(url);
await newPage.setViewport({ width: 1600, height: 789 });
return newPage;
}

async function setLanguageToEnglish() {
await clickSelector(page, 'button[aria-label="Change language"]');
await clickSelector(page, 'div[aria-label="Language selection modal"] a[data-testid="language-option-en"]');
}

async function login() {
console.log('-- Accessing Log in with email');
await clickXpath(page, '//button[contains(text(), "Log in with email")]');

console.log('Trying to log in');
console.log('-- Logging in');
/* The reason for the wait is because
anchorfm can take a little longer to load the form for logging in
and because pupeteer treats the page as loaded(or navigated to)
anchorfm can take a little longer to load the form for logging in
and because pupeteer treats the page as loaded(or navigated to)
even when the form is not showed
*/
await page.waitForSelector('#email');
await page.type('#email', env.ANCHOR_EMAIL);
await page.type('#password', env.ANCHOR_PASSWORD);
await clickSelector(page, 'button[type=submit]');
await navigationPromise;
console.log('Logged in');
await page.waitForNavigation();
console.log('-- Logged in');
}

console.log('Uploading audio file');
async function waitForNewEpisodeWizard() {
await sleepSeconds(1);
console.log('-- Waiting for episode wizard to open');
await page.waitForXPath('//span[contains(text(),"Select a file")]');
}

async function uploadEpisode() {
console.log('-- Uploading audio file');
await page.waitForSelector('input[type=file]');
const inputFile = await page.$('input[type=file]');
await inputFile.uploadFile(env.AUDIO_FILE);

console.log('Waiting for upload to finish');
await new Promise((r) => {
setTimeout(r, 25 * 1000);
});

await clickXpath(page, '//span[contains(text(),"Save")]/parent::button[not(boolean(@disabled))]', {
timeout: env.UPLOAD_TIMEOUT,
});
await navigationPromise;
console.log('-- Waiting for upload to finish');
await page.waitForXPath('//span[contains(text(),"Preview ready!")]', { timeout: env.UPLOAD_TIMEOUT });
console.log('-- Audio file is uploaded');
}

async function fillRequiredDetails() {
console.log('-- Adding title');
await page.waitForSelector('#title', { visible: true });
const titleInputSelector = '#title-input';
await page.waitForSelector(titleInputSelector, { visible: true });
// Wait some time so any field refresh doesn't mess up with our input
await new Promise((r) => {
setTimeout(r, 2000);
});
await page.type('#title', youtubeVideoInfo.title);
await sleepSeconds(2);
await page.type(titleInputSelector, youtubeVideoInfo.title);

console.log('-- Adding description');
await page.waitForSelector('div[role="textbox"]', { visible: true });
const textboxInputSelector = 'div[role="textbox"]';
await page.waitForSelector(textboxInputSelector, { visible: true });
const finalDescription = addUrlToDescription(youtubeVideoInfo);
await page.type('div[role="textbox"]', finalDescription);
if (isEmpty(finalDescription)) {
await page.type(textboxInputSelector, `Video: ${youtubeVideoInfo.url}`);
} else {
await page.type('div[role="textbox"]', finalDescription);
}

if (env.SET_PUBLISH_DATE) {
await setPublishDate(page, navigationPromise, youtubeVideoInfo.uploadDate);
const dateDisplay = `${youtubeVideoInfo.uploadDate.day} ${youtubeVideoInfo.uploadDate.monthAsFullWord}, ${youtubeVideoInfo.uploadDate.year}`;
console.log('-- Schedule publishing for date: ', dateDisplay);
await setPublishDate(page, youtubeVideoInfo.uploadDate);
} else {
console.log('-- No schedule, should publish immediately');
await clickSelector(page, 'input[type="radio"][id="publish-date-now"]');
}

console.log('-- Selecting content type');
console.log('-- Selecting content type(explicit or no explicit)');
const selectorForExplicitContentLabel = env.IS_EXPLICIT
? 'label[for="podcastEpisodeIsExplicit-true"]'
: 'label[for="podcastEpisodeIsExplicit-false"]';
? 'input[type="radio"][id="explicit-content"]'
: 'input[type="radio"][id="no-explicit-content"]';
await clickSelector(page, selectorForExplicitContentLabel, { visible: true });
}

async function fillOptionalDetails() {
console.log('-- Clicking Additional Details');
await clickXpath(page, '//button[contains(text(), "Additional details")]');

if (env.LOAD_THUMBNAIL) {
console.log('-- Uploading episode art');
Expand All @@ -157,69 +191,72 @@ async function postEpisode(youtubeVideoInfo) {
console.log('-- Saving uploaded episode art');
await clickXpath(page, '//span[text()="Save"]/parent::button');

console.log('-- Waiting for uploaded episode art to be saved');
await page.waitForXPath('//div[@aria-label="image uploader"]', { hidden: true, timeout: env.UPLOAD_TIMEOUT });
}
}

const saveDraftOrPublishOrScheduleButtonDescription = getSaveDraftOrPublishOrScheduleButtonDescription();
console.log(`-- ${saveDraftOrPublishOrScheduleButtonDescription.message}`);
await clickXpath(page, saveDraftOrPublishOrScheduleButtonDescription.xpath);
await navigationPromise;

/*
This is a workaround solution of the problem where the podcast
is sometimes saved as draft with title "Untitled" and no other metadata.
We navigate to the spotify/anchorfm dashboard immediately after podcast is
published/scheduled.
*/
await page.goto('https://podcasters.spotify.com/pod/dashboard/episodes');

console.log('Yay');
} catch (err) {
throw new Error(`Unable to post episode to anchorfm: ${err}`);
} finally {
if (browser !== undefined) {
await browser.close();
}
async function skipInteractStep() {
console.log('-- Going to Interact step so we can skip it');
await clickXpath(page, '//span[text()="Next"]/parent::button');
console.log('-- Waiting before clicking next again to skip Interact step');
await sleepSeconds(1);
console.log('-- Going to final step by skipping Interact step');
await clickXpath(page, '//span[text()="Next"]/parent::button');
}
}

function getSaveDraftOrPublishOrScheduleButtonDescription() {
if (env.SAVE_AS_DRAFT) {
return {
xpath: '//button[text()="Save as draft"]',
message: 'Saving draft',
};
async function saveDraftOrScheduleOrPublish() {
if (env.SAVE_AS_DRAFT) {
console.log('-- Saving draft');
await clickSelector(page, 'header > button > span');
await page.waitForNavigation();
await clickXpath(page, '//span[text()="Save draft"]/parent::button');
} else if (env.SET_PUBLISH_DATE) {
console.log('-- Scheduling');
await clickXpath(page, '//span[text()="Schedule"]/parent::button');
} else {
console.log('-- Publishing');
await clickXpath(page, '//span[text()="Publish"]/parent::button');
}
await sleepSeconds(3);
}

if (env.SET_PUBLISH_DATE) {
return {
xpath: '//span[text()="Schedule episode"]/parent::button',
message: 'Scheduling',
};
async function goToDashboard() {
await page.goto('https://podcasters.spotify.com/pod/dashboard/episodes');
await sleepSeconds(3);
}
}

return {
xpath: '//span[text()="Publish now"]/parent::button',
message: 'Publishing',
};
async function sleepSeconds(seconds) {
await new Promise((r) => {
setTimeout(r, seconds * 1000);
});
}

async function clickSelector(page, selector, options = {}) {
await page.waitForSelector(selector, options);
const element = await page.$(selector);
await clickDom(page, element);
const elementHandle = await page.$(selector);
await clickDom(page, elementHandle);
}

async function clickXpath(page, xpath, options = {}) {
await page.waitForXPath(xpath, options);
const [xpathBtn] = await page.$x(xpath);
await clickDom(page, xpathBtn);
const [elementHandle] = await page.$x(xpath);
await clickDom(page, elementHandle);
}

async function clickDom(page, domElementHandle) {
await page.evaluate((element) => element.click(), domElementHandle);
}

async function getTextContentFromSelector(page, selector, options = {}) {
await page.waitForSelector(selector, options);
const elementHandle = await page.$(selector);
return getTextContentFromDom(page, elementHandle);
}

async function clickDom(page, domBtn) {
await page.evaluate((elem) => {
elem.click();
}, domBtn);
async function getTextContentFromDom(page, domElementHandle) {
return page.evaluate((element) => element.textContent, domElementHandle);
}

module.exports = {
Expand Down
Loading

0 comments on commit 4208192

Please sign in to comment.