Spotify for Hackers: Run Node code to email new tracks on playlists
@dylan
code:
data:privatelast updated:3 years ago
today
Build integrations remarkably fast!
You're viewing a public workflow template.
Sign up to customize, add steps, modify code and more.
Join 800,000+ developers using the Pipedream platform
steps.
trigger
Cron Scheduler
Deploy to configure a custom schedule
This workflow runs on Pipedream's servers and is triggered on a custom schedule.
steps.
get_saved_playlists
auth
to use OAuth tokens and API keys in code via theauths object
(auths.spotify)
code
Write any Node.jscodeand use anynpm package. You can alsoexport datafor use in later steps via return or this.key = 'value', pass input data to your code viaparams, and maintain state across executions with$checkpoint.
async (event, steps, auths) => {
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
}
29
// Fetch all my playlists
let playlists = []
// Fetch the max of 50 playlists at a time
let url = `https://api.spotify.com/v1/me/playlists?limit=50`

do {
  const { items, next } = await require("@pipedreamhq/platform").axios(this, {
    method: "GET",
    url,
    params: {
      fields: "items(owner,external_urls,id,images,name,snapshot_id)"
    },
    headers: {
      // Use the OAuth access token from the Spotify OAuth grant.
      // See https://docs.pipedream.com/workflows/steps/code/auth/#the-auths-object
      Authorization: `Bearer ${auths.spotify.oauth_access_token}`,
    },
  })

  playlists = playlists.concat(items)
  url = next
} while (url)

// Return only playlists owned by others
this.playlists = playlists.filter(playlist => {
  return playlist.owner.id !== auths.spotify.oauth_uid
})
steps.
check_for_playlist_changes
auth
to use OAuth tokens and API keys in code via theauths object
(auths.spotify)
code
Write any Node.jscodeand use anynpm package. You can alsoexport datafor use in later steps via return or this.key = 'value', pass input data to your code viaparams, and maintain state across executions with$checkpoint.
async (event, steps, auths) => {
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
}
124
const axios = require("axios")
const _ = require("lodash")

// Keep track of changes by playlist ID
const changes = {}

// Retrieve the old state of the playlists from workflow state
// https://docs.pipedream.com/workflows/steps/code/#managing-state
const oldPlaylistState = this.$checkpoint || {}

// Fetch the newest state and save for the next run
newPlaylistState = await getCurrentPlaylistState(steps.get_saved_playlists.playlists)

if (_.isEmpty(oldPlaylistState)) {
  this.$checkpoint = newPlaylistState
  $end("Saved initial playlist data. Will check for changes on next run.")
}

// The snapshot_id changes with playlist changes. Compare the saved
// snapshot_id with the current to see what playlists changed
// https://developer.spotify.com/documentation/general/guides/working-with-playlists/#version-control-and-snapshots
if (!_.isEmpty(oldPlaylistState)) {
  const playlistsThatChanged = steps.get_saved_playlists.playlists.filter(playlist => {
    // If we have a new playlist (no old state stored for the playlist), move on
    if (!oldPlaylistState[playlist.id]) {
      return false
    }
    return oldPlaylistState[playlist.id].snapshot_id !== playlist.snapshot_id
  })

  if (!playlistsThatChanged.length) {
    this.$checkpoint = newPlaylistState
    $end("No playlists changed")
  }

  // For playlists that changed, find the diff.
  // We only care about songs that are in the new state and not in the old.
  for (const playlist of playlistsThatChanged) {
    const { id, images, name, external_urls } = playlist
    const oldTracks = _.sortBy(oldPlaylistState[id].tracks, 'track.id')
    const newTracks = _.sortBy(newPlaylistState[id].tracks, 'track.id')
    const trackDiff = _.differenceBy(newTracks, oldTracks, 'track.id')
    if (!trackDiff.length) {
      continue
    }
    changes[id] = {
      images,
      link: external_urls.spotify,
      name,
      trackDiff,
    }
  }

  if (_.isEmpty(changes)) {
    this.$checkpoint = newPlaylistState
    $end("No new tracks")
  }

  // Format and send email
  formatAndSendEmail(changes)
}

this.$checkpoint = newPlaylistState

async function getCurrentPlaylistState(playlists) {
  const currentPlaylistState = {}
  for (const playlist of playlists) {
    const { id, external_urls, images, name, snapshot_id } = playlist
    let tracks = []
    // Fetch the max of 100 tracks at a time
    let url = `https://api.spotify.com/v1/playlists/${id}/tracks?limit=100`

    do {
      const { items, next } = (await axios({
        method: "GET",
        url,
        params: {
          fields: "items(track(id,name,artists))"
        },
        headers: {
          Authorization: `Bearer ${auths.spotify.oauth_access_token}`,
        },
      })).data

      tracks = tracks.concat(items)
      url = next
    } while (url)

    currentPlaylistState[id] = {
      link: external_urls.spotify,
      images,
      name,
      snapshot_id,
      tracks,
    }
  }

  return currentPlaylistState
}

function formatAndSendEmail(changes) {
  let html = ''
  for (let [id, playlist] of Object.entries(changes)) {
    const { link, images, name, trackDiff } = playlist
    html += `<strong>New tracks on <a href="${link}">${name}</a></strong><br><br>
    <img width="200px" alt="${name}" src="${images[0].url}"><br><br>
    
    ${trackDiff.map(t => {
      const { name, artists } = t.track
      const artistString = artists.map(a => a.name).join(', ')
      return `${name} by ${artistString}`
    }).join('<br>')}
    <br><br>`
  }

  // Send yourself an email
  // https://docs.pipedream.com/destinations/email/#adding-an-email-destination
  $send.email({
    subject: "Updates to Spotify playlists you follow",
    html,
  })
}