Created
February 22, 2021 14:07
-
-
Save RylandDeGregory/b3eb7a59cdb904181eda763f6fbe69a9 to your computer and use it in GitHub Desktop.
Export Spotify playlists to .csv file using PowerShell. Uses Spotify web API with OAuth2 Client Authorization flow
This file contains 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
<# | |
.SYNOPSIS | |
Export Spotify playlists to .csv file for backup | |
.DESCRIPTION | |
Export Spotify playlists to .csv file using the Spotify web API with OAuth2 Client Authorization flow | |
.EXAMPLE | |
./Start-SpotifyPlaylistExport.ps1 -KeyVaultName 'myazkeyvault' -PlaylistType 'User' | |
.NOTES | |
- Assumes that a Spotify application has been configured and an OAuth2 Refresh token has been granted for a user | |
https://developer.spotify.com/documentation/general/guides/authorization-guide/ | |
- Assumes that an Azure Key Vault has been configured to store API secrets | |
https://docs.microsoft.com/en-us/azure/key-vault/secrets/quick-create-portal | |
.LINK | |
https://ryland.dev | |
#> | |
#region Init | |
[CmdletBinding()] | |
param ( | |
# The name of the Azure Key Vault where Spotify API secrets are stored | |
[Parameter(Mandatory)] | |
[string] $KeyVaultName, | |
# Process only user-created playlists, only followed playlists, or all playlists | |
[Parameter(Mandatory)] | |
[ValidateSet('User', 'Followed', 'All')] | |
[string] $PlaylistType | |
) | |
# Properties that will be returned for each track | |
# https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-playlists-tracks | |
$TrackFields = 'items(added_at,added_by.id,track(name,id,external_urls(spotify),artists(name,external_urls(spotify)),album(name,external_urls(spotify))))' | |
# File system location for the resulting .csv file containing playlist data | |
$OutputFileLocation = "$HOME/Desktop/PlaylistExport_$(Get-Date -Format 'yyyy-MM-dd').csv" | |
#endregion Init | |
#region Functions | |
function Get-SpotifyAccessToken { | |
<# | |
.SYNOPSIS | |
Get an OAuth2 Access token from an OAuth2 Refresh token and an application Client ID and Client Secret | |
OAuth2 Access token is used to access the Spotify API | |
#> | |
[CmdletBinding()] | |
param ( | |
# The name of the Azure Key Vault where Spotify API secrets are stored | |
[Parameter(Mandatory)] | |
[string] $KeyVaultName | |
) | |
begin { | |
# Application credentials | |
$ClientId = Get-AzKeyVaultSecret -VaultName $KeyVaultName -SecretName 'Spotify-ClientID' -AsPlainText | |
$ClientSecret = Get-AzKeyVaultSecret -VaultName $KeyVaultName -SecretName 'Spotify-ClientSecret' -AsPlainText | |
$ApplicationCredentials = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("$ClientId`:$ClientSecret")) | |
# User credentials | |
$RefreshToken = Get-AzKeyVaultSecret -VaultName $KeyVaultName -SecretName 'Spotify-RefreshToken' -AsPlainText | |
} | |
process { | |
# Request elements | |
$TokenHeader = @{ 'Authorization' = "Basic $ApplicationCredentials" } | |
$TokenBody = @{ grant_type = 'refresh_token'; refresh_token = "$RefreshToken" } | |
try { | |
# Get an Access token from the Refresh token | |
$AccessToken = Invoke-RestMethod -Method Post -Headers $TokenHeader -Uri 'https://accounts.spotify.com/api/token' -Body $TokenBody | |
} catch { | |
throw "[ERROR] Error getting Access Token from Spotify API '/token' endpoint using Refresh Token: $($Error[0])" | |
} | |
} | |
end { | |
# If an Access token was granted, return it | |
if ($AccessToken) { | |
return $AccessToken.access_token | |
} else { | |
return $null | |
} | |
} | |
} #endfunction Get-SpotifyAccessToken | |
#endregion Functions | |
#region GetPlaylists | |
# Get OAuth2 Access token and set API headers | |
$AccessToken = Get-SpotifyAccessToken -KeyVaultName $KeyVaultName | |
if ($AccessToken) { | |
$Headers = @{ 'Authorization' = "Bearer $AccessToken" } | |
} else { | |
throw '[ERROR] No OAuth2 Access token was granted. Please ensure that the application ClientID and ClientSecret, and the user OAuth2 Refresh token are valid.' | |
} | |
try { | |
# Get the authenticated user's Spotify profile | |
$User = Invoke-RestMethod -Method Get -Headers $Headers -Uri 'https://api.spotify.com/v1/me/' | |
Write-Verbose "[INFO] Processing playlists for Spotify user $($User.display_name)" | |
} catch { | |
throw "[ERROR] Error getting the authenticated user's Spotify profile: $($Error[0])" | |
} | |
# Determine the user's number of playlists and calculate the number of paginated requests to make | |
try { | |
$PlaylistCount = Invoke-RestMethod -Method Get -Headers $Headers -Uri 'https://api.spotify.com/v1/me/playlists?limit=1' | |
} catch { | |
throw "[ERROR] Error getting the number of playlists for user: $($Error[0])" | |
} | |
$PlaylistPages = [math]::ceiling($PlaylistCount.total / 50) | |
# Build collection of playlists by processing all pages | |
$Playlists = for ($i = 0; $i -lt $PlaylistPages; $i++) { | |
try { | |
Invoke-RestMethod -Method Get -Headers $Headers -Uri "https://api.spotify.com/v1/me/playlists?limit=50&offset=$($i * 50)" | |
} catch { | |
throw "[ERROR] Error getting list of playlists for user: $($Error[0])" | |
} | |
} | |
# Process playlist types based on parameter input | |
$ProcessPlaylists = switch ($PlaylistType) { | |
'User' { $Playlists.items | Where-Object { $_.owner.id -eq $User.id } } | |
'Followed' { $Playlists.items | Where-Object { $_.owner.id -ne $User.id } } | |
'All' { $Playlists.items } | |
} | |
#endregion GetPlaylists | |
#region ProcessPlaylists | |
$TrackArray = foreach ($Playlist in $ProcessPlaylists) { | |
Write-Verbose "[INFO] Processing playlist [$($Playlist.name)]" | |
# Calculate the number of paginated requests to make to get all tracks in the playlist | |
$TrackPages = [math]::ceiling($Playlist.tracks.total / 100) | |
# Build collection of tracks by processing all pages | |
for ($i = 0; $i -lt $TrackPages; $i++) { | |
try { | |
# Get all tracks in the playlist page, the API returns only the pre-defined fields | |
$Tracks = Invoke-RestMethod -Method Get -Headers $Headers -Uri "https://api.spotify.com/v1/playlists/$($Playlist.id)/tracks?limit=100&offset=$($i * 100)&fields=$TrackFields" | |
} catch { | |
throw "[ERROR] Error getting tracks from playlist [$($Playlist.name)]: $($Error[0])" | |
} | |
# Create object for each track in playlist with processed data | |
foreach ($Track in $Tracks.items) { | |
[PSCustomObject]@{ | |
PlaylistName = $Playlist.name -replace '[^a-zA-Z0-9 ]', '' | |
PlaylistURL = $Playlist.external_urls.spotify | |
AddedAt = $Track.added_at | |
AddedBy = $Track.added_by.id | |
Name = $Track.track.name | |
TrackURL = $Track.track.external_urls.spotify | |
Artist = $Track.track.artists.name | Join-String -Separator ', ' | |
ArtistURL = $Track.track.artists.external_urls.spotify | Join-String -Separator ', ' | |
Album = $Track.track.album.name | |
AlbumURL = $Track.track.album.external_urls.spotify | |
} | |
} | |
} | |
} | |
#endregion ProcessPlaylists | |
#region Output | |
# Export collection of tracks from all processed playlists to a .csv file at the pre-defined path | |
$TrackArray | Export-Csv -Path $OutputFileLocation -NoTypeInformation -Force | |
#endregion Output |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment