diff --git a/README.md b/README.md index ae028293..b1196bf8 100644 --- a/README.md +++ b/README.md @@ -173,9 +173,12 @@ env: It is possible to use a single repository to maintain several shows. -You'll need an episode config per show. +There are two ways to do that. -As an example, suppose you have two shows, you called "Great News" and another "Sad News". +### You have multiple accounts. + +As an example, suppose you have two shows, you called "Great News" and another "Sad News". Every +show on its own account. You repository will look like this: @@ -218,6 +221,35 @@ jobs: # (…) Other configs as needed ``` +### You have multiple podcasts on the same account + +Same as before, you setup one config per podcast, but with the same `SPOTIFY_EMAIL` and `SPOTIFY_PASSWORD`. +But now you provide a title for the podcast, to that it can be found in the list: + +In `great-news.yaml` and `sad-news.yaml`: +```yaml +name: 'Great News Upload Action' +on: + push: + paths: + ## only updates to this file trigger this action + - great-news.json # or sad-news.json +jobs: + upload_episode: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Upload Episode from YouTube To Spotify + uses: Schrodinger-Hat/youtube-to-spotify@v2.5.0 + env: + SPOTIFY_EMAIL: ${{ secrets.SPOTIFY_EMAIL }} # Both shows on the same account + SPOTIFY_PASSWORD: ${{ secrets.SPOTIFY_PASSWORD }} + PODCAST_TITLE: Great News # Or "Sad News", just as shown in your spotify podcasts list + EPISODE_PATH: /github/workspace/ + EPISODE_FILE: great-news.json # or sad-news.json + # (…) Other configs as needed +``` + ## How can I setup for development and use the script locally? To run the script locally, you need `python3` and `ffmpeg` to be available in `PATH` which are used by the npm dependency `youtube-dl-exec`. diff --git a/src/environment-variables/index.js b/src/environment-variables/index.js index ed63ab75..c5fe9662 100644 --- a/src/environment-variables/index.js +++ b/src/environment-variables/index.js @@ -20,6 +20,7 @@ const defaultValues = { THUMBNAIL_FILE_FORMAT: 'jpg', THUMBNAIL_FILE_TEMPLATE: 'thumbnail.%(ext)s', PUPPETEER_HEADLESS: true, + PODCAST_TITLE: '' }; const dotEnvVariables = parseDotEnvVariables(); @@ -70,6 +71,7 @@ module.exports = { SPOTIFY_PASSWORD: getEnvironmentVariable('SPOTIFY_PASSWORD'), SPOTIFY_EMAIL: getEnvironmentVariable('SPOTIFY_EMAIL'), SPOTIFY_PASSWORD: getEnvironmentVariable('SPOTIFY_PASSWORD'), + PODCAST_TITLE: getEnvironmentVariable('PODCAST_TITLE'), UPLOAD_TIMEOUT: getEnvironmentVariable('UPLOAD_TIMEOUT'), SAVE_AS_DRAFT: getBoolean(getEnvironmentVariable('SAVE_AS_DRAFT')), LOAD_THUMBNAIL: getBoolean(getEnvironmentVariable('LOAD_THUMBNAIL')), diff --git a/src/spotify-puppeteer/index.js b/src/spotify-puppeteer/index.js index e1255a08..0a916adc 100644 --- a/src/spotify-puppeteer/index.js +++ b/src/spotify-puppeteer/index.js @@ -46,6 +46,43 @@ async function setPublishDate(page, date) { } } +async function selectPodcast(page, targetTitle) { + // When we come to this point in the flow, the dashboard page + // is in a state it never reaches when using interatively: it has + // no podcast selected and all the podcasts are in the list, available to be selected. + // + // Normally, when whe access the creators dashboard, there is a podcast already selected + // and others available podcasts to be selected. + // + // This helped us here, because we can simply choose and click on any podcast on the list. + // But note that if the user has not provided a podcast title, it means she wants + // the first one, so we cannot enter this state and just follow from the state the + // page is before entering here. + if (env.PODCAST_TITLE === '') { + logger.info('-- No PODCAST_TITLE provided. Using default podcast.'); + return + } + + var navigationPromise = page.waitForNavigation(); + await page.goto('https://podcasters.spotify.com/pod/dashboard/episodes') + await navigationPromise; + + logger.info(`-- Searching for podcast ${targetTitle}`); + const elementHandle = await page.waitForFunction(` + document.querySelector('#__chrome').shadowRoot.querySelector("a[aria-label='${targetTitle}']") + `); + + logger.info(`-- Selected podcast ${targetTitle}`); + navigationPromise = page.waitForNavigation(); + await elementHandle.asElement().click() + await navigationPromise; + + logger.info('-- Going back to the new episode wizard'); + navigationPromise = page.waitForNavigation(); + page = await page.goto('https://creators.spotify.com/pod/dashboard/episode/wizard'); + await navigationPromise; +} + async function postEpisode(youtubeVideoInfo) { let browser; let page; @@ -65,6 +102,9 @@ async function postEpisode(youtubeVideoInfo) { logger.info('Trying to log in and open episode wizard'); await loginAndWaitForNewEpisodeWizard(); + logger.info(`Making sure the correct podcast is selected for uploading`); + await selectPodcast(page, env.PODCAST_TITLE); + logger.info('Uploading audio file'); await uploadEpisode(); @@ -131,12 +171,6 @@ async function postEpisode(youtubeVideoInfo) { async function loginAndWaitForNewEpisodeWizard() { await spotifyLogin(); - try { - logger('-- Waiting for navigation after logging in'); - await page.waitForNavigation(); - } catch (err) { - logger.info('-- The wait for navigation after logging failed or timed-out. Continuing.'); - } return Promise.any([acceptSpotifyAuth(), waitForNewEpisodeWizard()]).then((res) => { if (res === SPOTIFY_AUTH_ACCEPTED) { @@ -150,14 +184,18 @@ async function postEpisode(youtubeVideoInfo) { async function spotifyLogin() { logger.info('-- Accessing new Spotify login page for podcasts'); + var navigationPromise = page.waitForNavigation(); await clickSelector(page, '::-p-xpath(//span[contains(text(), "Continue with Spotify")]/parent::button)'); - logger.info('-- Logging in'); + await navigationPromise; + logger.info('-- Logging in'); await page.waitForSelector('#login-username'); await page.type('#login-username', env.SPOTIFY_EMAIL); await page.type('#login-password', env.SPOTIFY_PASSWORD); - await sleepSeconds(1); + + navigationPromise = page.waitForNavigation(); await clickSelector(page, 'button[id="login-button"]'); + await navigationPromise; } function acceptSpotifyAuth() { @@ -269,8 +307,10 @@ async function postEpisode(youtubeVideoInfo) { } async function goToDashboard() { - await page.goto('https://podcasters.spotify.com/pod/dashboard/episodes'); - await sleepSeconds(3); + logger.info("-- Going to dashboard"); + var navigationPromise = page.waitForNavigation(); + page.goto('https://podcasters.spotify.com/pod/dashboard/episodes') + await navigationPromise; } }