Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple shows with a single spotify account #140

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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/[email protected]
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`.
Expand Down
2 changes: 2 additions & 0 deletions src/environment-variables/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const defaultValues = {
THUMBNAIL_FILE_FORMAT: 'jpg',
THUMBNAIL_FILE_TEMPLATE: 'thumbnail.%(ext)s',
PUPPETEER_HEADLESS: true,
PODCAST_TITLE: ''
};

const dotEnvVariables = parseDotEnvVariables();
Expand Down Expand Up @@ -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')),
Expand Down
60 changes: 50 additions & 10 deletions src/spotify-puppeteer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();

Expand Down Expand Up @@ -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) {
Expand All @@ -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() {
Expand Down Expand Up @@ -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;
}
}

Expand Down